diff --git a/activity_store.py b/activity_store.py index 0f971d2..a72c07e 100644 --- a/activity_store.py +++ b/activity_store.py @@ -4,7 +4,7 @@ import Xlib.error -import hook_manager +import sniff_x import models from models import Process, Window, Geometry, Click, Keys @@ -12,17 +12,17 @@ """ Todo: - change name to selfspy -- - make unthreaded + change git name to selfspy -- optional crypto on Keys.text and Keys.timings timings in json compress text and timings (check size difference on existing db) - ask for pw in tk, if not command line -- simple utility for reading and stats + take pw from default config file, if exists -- + ask for pw in tk, if not command line +- README - test map switch @@ -63,16 +63,16 @@ def __init__(self, db_name): self.cur_win_id = None def run(self): - self.hm = hook_manager.HookManager() + self.sniffer = sniff_x.SniffX() self.log_cur_window() - self.hm.key_hook = self.got_key - self.hm.mouse_button_hook = self.got_mouse_click - self.hm.mouse_move_hook = self.got_mouse_move + self.sniffer.key_hook = self.got_key + self.sniffer.mouse_button_hook = self.got_mouse_click + self.sniffer.mouse_move_hook = self.got_mouse_move - self.hm.start() + self.sniffer.run() def close(self): - self.hm.cancel() + self.sniffer.cancel() def store_window(self): cur_window = self.session.query(Window).filter_by(title=self.cur_name.decode('latin1'), process_id=self.cur_process_id).scalar() @@ -114,7 +114,7 @@ def get_cur_window(self): i = 0 while True: try: - cur_window = self.hm.the_display.get_input_focus().focus + cur_window = self.sniffer.the_display.get_input_focus().focus cur_class = None cur_name = None while cur_class is None and cur_class is None: diff --git a/hook_manager.py b/hook_manager.py index 9ae8093..6fc99c8 100644 --- a/hook_manager.py +++ b/hook_manager.py @@ -18,11 +18,8 @@ def state_to_idx(state): #this could be a dict, but after improvements a dict wi if state == 129: return 5 return 0 -class HookManager(threading.Thread): +class SniffX: def __init__(self): - threading.Thread.__init__(self) - self.finished = threading.Event() - self.keysymdict = {} for name in dir(XK): if name.startswith("XK_"): @@ -69,7 +66,6 @@ def run(self): self.record_display.record_free_context(self.ctx) def cancel(self): - self.finished.set() self.the_display.record_disable_context(self.ctx) self.the_display.flush() diff --git a/selfspy.py b/selfspy.py index 5d8785a..30b1c47 100755 --- a/selfspy.py +++ b/selfspy.py @@ -44,14 +44,17 @@ def parse_config(): if __name__ == '__main__': args = vars(parse_config()) + try: args['gid'] = int(args['gid']) except ValueError: args['gid'] = grp.getgrnam(args['gid']) + try: args['uid'] = int(args['uid']) except ValueError: args['uid'] = pwd.getpwnam(args['uid']).pw_gid + print args lock = lockfile.FileLock(args['lock_file']) @@ -74,12 +77,10 @@ def parse_config(): signal.SIGTERM: 'terminate', signal.SIGHUP: 'terminate' } - - try: - with context: - astore = ActivityStore(os.path.join(args['data_dir'], DBNAME)) + + with context: + astore = ActivityStore(os.path.join(args['data_dir'], DBNAME)) + try: astore.run() - while True: - time.sleep(1000000000) - except SystemExit: - astore.close() + except SystemExit: + astore.close() diff --git a/sniff_x.py b/sniff_x.py new file mode 100644 index 0000000..980d02d --- /dev/null +++ b/sniff_x.py @@ -0,0 +1,116 @@ +# This file is loosely based on examples/record_demo.py in python-xlib + +import sys +import os +import re +import time +import threading + +from Xlib import X, XK, display +from Xlib.ext import record +from Xlib.protocol import rq + +def state_to_idx(state): #this could be a dict, but I might want to extend it. + if state == 1: return 1 + if state == 128: return 4 + if state == 129: return 5 + return 0 + +class SniffX: + def __init__(self): + self.keysymdict = {} + for name in dir(XK): + if name.startswith("XK_"): + self.keysymdict[getattr(XK, name)] = name[3:] + + self.key_hook = lambda x: True + self.mouse_button_hook = lambda x: True + self.mouse_move_hook = lambda x: True + + self.contextEventMask = [X.KeyPress, X.MotionNotify] #X.MappingNotify? + + self.the_display = display.Display() + self.record_display = display.Display() + self.keymap = self.the_display._keymap_codes + + def run(self): + # Check if the extension is present + if not self.record_display.has_extension("RECORD"): + print "RECORD extension not found" + sys.exit(1) + else: + print "RECORD extension present" + + # Create a recording context; we only want key and mouse events + self.ctx = self.record_display.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), + '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_display.record_enable_context(self.ctx, self.processevents) + # Finally free the context + self.record_display.record_free_context(self.ctx) + + def cancel(self): + self.the_display.record_disable_context(self.ctx) + self.the_display.flush() + + 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_display.display, None, None) + if event.type in [X.KeyPress, X.KeyRelease]: + self.key_hook(*self.key_event(event)) + elif event.type in [X.ButtonPress, X.ButtonRelease]: + self.mouse_button_hook(*self.button_event(event)) + elif event.type == X.MotionNotify: + self.mouse_move_hook(event.root_x, event.root_y) + elif event.type == X.MappingNotify: + self.the_display.refresh_keyboard_mapping() + newkeymap = self.the_display._keymap_codes + print 'Change keymap!', newkeymap == self.keymap + self.keymap = newkeymap + + + def get_key_name(self, keycode, state): + state_idx = state_to_idx(state) + cn = self.keymap[keycode][state_idx] + if cn < 256: + return chr(cn).decode('latin1')#.encode('utf8') + else: + return self.lookup_keysym(cn) + + def key_event(self, event): + return event.detail, event.state, self.get_key_name(event.detail, event.state), event.type == X.KeyPress + + def button_event(self, event): + return event.detail, event.type == X.ButtonPress + + def lookup_keysym(self, keysym): + if keysym in self.keysymdict: + return self.keysymdict[keysym] + return "[%d]" % keysym + + + +