diff --git a/ereshkigal.py b/ereshkigal.py index 492899e..032759e 100755 --- a/ereshkigal.py +++ b/ereshkigal.py @@ -30,6 +30,7 @@ import logging import psutil import socket import re +import collections class Tunnel: @@ -89,16 +90,16 @@ class RawTunnel(Tunnel): class Connection: """A dictionary that stores an SSH connection related to a tunnel""" - def __init__(self, local_address = None, local_port = None, foreign_address = None, foreign_port = None, + def __init__(self, local_address = None, in_port = None, foreign_address = None, out_port = None, status = None, family = None ): # informations available with netstat assert(local_address!=None) self.local_address = local_address - assert(local_port!=None) - self.local_port = local_port + assert(in_port!=None) + self.in_port = in_port self.foreign_address = foreign_address - self.foreign_port = foreign_port + self.out_port = out_port assert(status!=None) self.status = status assert(family!=None) @@ -111,19 +112,19 @@ class Connection: def __repr__(self): # do not logging.debug all the informations by default - if self.foreign_address and self.foreign_port: + if self.foreign_address and self.out_port: return "%s:%i -> %s:%i\t%s\t%s" % ( self.local_address, - self.local_port, + self.in_port, self.foreign_address, - self.foreign_port, + self.out_port, self.family_rep[self.family], self.status, ) else: return "%s:%i\t%s\t%s" % ( self.local_address, - self.local_port, + self.in_port, self.family_rep[self.family], self.status, ) @@ -134,7 +135,7 @@ class TunnelsParser: """Warning: the initialization does not gather tunnels informations, use update() to do so""" # { ssh_pid : Tunnel } - self.tunnels = {} + self.tunnels = collections.OrderedDict() # do not perform update by default # this is necessary because one may want @@ -145,6 +146,12 @@ class TunnelsParser: self.header = 'TYPE\tPID\tIN_PORT\tVIA_HOST\tTARGET_HOST\tOUT_PORT' + + def get_tunnel(self, pos): + pid = list(self.tunnels.keys())[pos] + return self.tunnels[pid] + + def parse(self, cmd): cmdline = " ".join(cmd) @@ -253,7 +260,7 @@ import curses import time import signal -class monitorCurses: +class CursesMonitor: """Textual user interface to display up-to-date informations about current tunnels""" def __init__(self, scr): @@ -261,7 +268,7 @@ class monitorCurses: self.scr = scr # tunnels monitor - self.tm = TunnelMonitor() + self.tp = TunnelsParser() # selected line self.cur_line = -1 @@ -270,7 +277,7 @@ class monitorCurses: self.cur_pid = -1 # switch to show only autoss processes (False) or ssh connections also (True) - self.show_tunnels = False + self.show_connections = False # FIXME pass as parameters+options self.update_delay = 1 # seconds of delay between two data updates @@ -278,9 +285,9 @@ class monitorCurses: # colors # FIXME different colors for different types of tunnels (auto or raw) - self.colors_tunnel = {'kind':4, 'pid':0, 'local_port':3, 'via_host':2, 'target_host':2, 'foreign_port':3, 'tunnels_nb':4, 'tunnels_nb_none':1} - self.colors_highlight = {'kind':9, 'pid':9, 'local_port':9, 'via_host':9, 'target_host':9, 'foreign_port':9, 'tunnels_nb':9, 'tunnels_nb_none':9} - self.colors_connection = {'ssh_pid':0, 'status':4, 'status_out':1, 'local_address':2, 'local_port':3, 'foreign_address':2, 'foreign_port':3} + self.colors_tunnel = {'kind':4, 'autossh_pid':0, 'in_port':3, 'via_host':2, 'target_host':2, 'out_port':3, 'tunnels_nb':4, 'tunnels_nb_none':1} + self.colors_highlight = {'kind':9, 'autossh_pid':9, 'in_port':9, 'via_host':9, 'target_host':9, 'out_port':9, 'tunnels_nb':9, 'tunnels_nb_none':9} + self.colors_connection = {'ssh_pid':0, 'autossh_pid':0, 'status':4, 'status_out':1, 'local_address':2, 'in_port':3, 'foreign_address':2, 'out_port':3} def __call__(self): @@ -306,16 +313,16 @@ class monitorCurses: # if its time to update if time.time() > last_update + self.update_delay: - self.tm.update() + self.tp.update() # reset the counter last_update = time.time() - state = "%s" % self.tm + state = "%s" % self.tp if state != last_state: logging.debug("Waited: %s" % log_ticks) log_ticks = "" logging.debug("----- Time of screen update: %s -----" % time.time()) - logging.debug("State of tunnels:\n%s" % self.tm) + logging.debug("State of tunnels:\n%s" % self.tp) last_state = state else: log_ticks += "." @@ -347,7 +354,7 @@ class monitorCurses: # if a pid is selected if self.cur_pid != -1: # send the SIGUSR1 signal - if self.tm[self.cur_line]['kind'] == 'auto': + if type(self.tp.get_tunnel(self.cur_line)) == AutoTunnel: # autossh performs a reload of existing tunnels that it manages logging.debug("SIGUSR1 on PID: %i" % self.cur_pid) os.kill( self.cur_pid, signal.SIGUSR1 ) @@ -364,28 +371,29 @@ class monitorCurses: # the related process is stopped # FIXME SIGTERM or SIGKILL ? - # tunnel = self.tm[self.cur_line] - # if tunnel['kind'] == 'auto': - # # FIXME kill SSH first - # logging.debug("SIGKILL on ssh PID: %i" % tunnel['ssh_pid']) - # try: - # os.kill( tunnel['ssh_pid'], signal.SIGKILL ) - # except OSError: - # logging.error("No such process: %i" % tunnel['ssh_pid']) + tunnel = self.tp.get_tunnel(self.cur_line) + if type(tunnel) == AutoTunnel: + logging.debug("SIGKILL on autossh PID: %i" % self.cur_pid) + try: + os.kill( self.cur_pid, signal.SIGKILL ) + except OSError: + logging.error("No such process: %i" % self.cur_pid) - logging.debug("SIGKILL on autossh PID: %i" % self.cur_pid) + logging.debug("SIGKILL on ssh PID: %i" % tunnel.ssh_pid) try: - os.kill( self.cur_pid, signal.SIGKILL ) + os.kill( tunnel.ssh_pid, signal.SIGKILL ) except OSError: - logging.error("No such process: %i" % self.cur_pid) + logging.error("No such process: %i" % tunnel.ssh_pid) + # FIXME update cur_pid or get rid of it everywhere + # Switch to show ssh connections # only available for root - elif ch in 'tT' and os.getuid() == 0: + elif ch in 'tT':# and os.getuid() == 0: logging.debug("Waited: %s" % log_ticks) log_ticks = "" logging.debug("Key pushed: T") - self.show_tunnels = not self.show_tunnels + self.show_connections = not self.show_connections # key pushed elif kc == curses.KEY_DOWN: @@ -393,10 +401,13 @@ class monitorCurses: log_ticks = "" logging.debug("Key pushed: down") # if not the end of the list - if self.cur_line < len(self.tm)-1: + if self.cur_line < len(self.tp.tunnels)-1: self.cur_line += 1 # get the pid - self.cur_pid = int(self.tm[self.cur_line]['pid']) + if type(self.tp.get_tunnel(self.cur_line)) == AutoTunnel: + self.cur_pid = self.tp.get_tunnel(self.cur_line).autossh_pid + else: + self.cur_pid = self.tp.get_tunnel(self.cur_line).ssh_pid # key up elif kc == curses.KEY_UP: @@ -406,7 +417,7 @@ class monitorCurses: if self.cur_line > -1: self.cur_line -= 1 if self.cur_line > 0: - self.cur_pid = int(self.tm[self.cur_line]['pid']) + self.cur_pid = self.tp.get_tunnel(self.cur_line).pid else: # do nothing and wait until the next refresh @@ -426,8 +437,8 @@ class monitorCurses: # First line: help help_msg = "[R]:reload autossh [K]:kill tunnel [Q]:quit" - if os.geteuid() == 0: - help_msg += " [T]:show network connections" + # if os.geteuid() == 0: + help_msg += " [T]:show network connections" help_msg += '\n' self.scr.addstr(0,0, help_msg, curses.color_pair(4) ) @@ -435,9 +446,9 @@ class monitorCurses: # Second line self.scr.addstr( "Active tunnels: ", curses.color_pair(6) ) - self.scr.addstr( str( len(self.tm) ), curses.color_pair(1) ) + self.scr.addstr( str( len(self.tp.tunnels) ), curses.color_pair(1) ) self.scr.addstr( " / Active connections: ", curses.color_pair(6) ) - self.scr.addstr( str( sum([len(i['connections']) for i in self.tm]) ), curses.color_pair(1) ) + self.scr.addstr( str( sum([len(self.tp.tunnels[t].connections) for t in self.tp.tunnels]) ), curses.color_pair(1) ) self.scr.addstr( '\n', curses.color_pair(1) ) self.scr.clrtoeol() @@ -450,18 +461,18 @@ class monitorCurses: # header line header_msg = "TYPE\tPID \tINPORT\tVIA \tTARGET \tOUTPORT" - if os.geteuid() == 0: - header_msg += "\tCONNECTIONS" + # if os.geteuid() == 0: + header_msg += "\tCONNECTIONS" self.scr.addstr( header_msg, curses.color_pair(color) ) self.scr.clrtoeol() # for each autossh processes available in the monitor - for l in range(len(self.tm)): + for l in range(len(self.tp.tunnels)): # add a line for the l-th autossh process self.add_autossh( l ) # if one want to show connections - if self.show_tunnels and os.getuid() == 0: + if self.show_connections:# and os.getuid() == 0: self.add_connection( l ) self.scr.clrtobot() @@ -473,29 +484,29 @@ class monitorCurses: colors = self.colors_connection # for each connections related to te line-th autossh process - for t in self.tm[line]['connections']: + for t in self.tp.get_tunnel(line).connections: # FIXME fail if the screen's height is too small. self.scr.addstr( '\n\t+ ' ) # self.scr.addstr( str( t['ssh_pid'] ), curses.color_pair(colors['ssh_pid'] ) ) # self.scr.addstr( '\t' ) - self.scr.addstr( str( t['local_address'] ) , curses.color_pair(colors['local_address'] )) + self.scr.addstr( str( t.local_address ) , curses.color_pair(colors['local_address'] )) self.scr.addstr( ':' ) - self.scr.addstr( str( t['local_port'] ) , curses.color_pair(colors['local_port'] )) + self.scr.addstr( str( t.in_port ) , curses.color_pair(colors['in_port'] )) self.scr.addstr( ' -> ' ) - self.scr.addstr( str( t['foreign_address'] ) , curses.color_pair(colors['foreign_address'] )) + self.scr.addstr( str( t.foreign_address ) , curses.color_pair(colors['foreign_address'] )) self.scr.addstr( ':' ) - self.scr.addstr( str( t['foreign_port'] ) , curses.color_pair(colors['foreign_port'] )) + self.scr.addstr( str( t.out_port ) , curses.color_pair(colors['out_port'] )) self.scr.addstr( '\t' ) color = self.colors_connection['status'] # if the connections is established # TODO avoid hard-coded constants - if t['status'] != 'ESTABLISHED': + if t.status != 'ESTABLISHED': color = self.colors_connection['status_out'] - self.scr.addstr( t['status'], curses.color_pair( color ) ) + self.scr.addstr( t.status, curses.color_pair( color ) ) self.scr.clrtoeol() @@ -503,28 +514,35 @@ class monitorCurses: def add_autossh(self, line): """Add line corresponding to the line-th autossh process""" self.scr.addstr( '\n' ) - self.add_autossh_info('kind', line) - self.add_autossh_info('pid', line) - self.add_autossh_info('local_port', line) + + if type(self.tp.get_tunnel(line)) == AutoTunnel: + self.scr.addstr( 'auto', curses.color_pair(self.colors_tunnel['kind']) ) + self.scr.addstr( '\t', curses.color_pair(self.colors_tunnel['kind']) ) + else: + self.scr.addstr( 'ssh', curses.color_pair(self.colors_tunnel['kind']) ) + self.scr.addstr( '\t', curses.color_pair(self.colors_tunnel['kind']) ) + + self.add_autossh_info('autossh_pid', line) + self.add_autossh_info('in_port', line) self.add_autossh_info('via_host', line) self.add_autossh_info('target_host', line) - self.add_autossh_info('foreign_port', line) + self.add_autossh_info('out_port', line) - nb = len(self.tm[line]['connections'] ) + nb = len(self.tp.get_tunnel(line).connections ) if nb > 0: # for each connection related to this process - for i in self.tm[line]['connections']: + for i in self.tp.get_tunnel(line).connections: # add a vertical bar | # the color change according to the status of the connection - if i['status'] == 'ESTABLISHED': + if i.status == 'ESTABLISHED': self.scr.addstr( '|', curses.color_pair(self.colors_connection['status']) ) else: self.scr.addstr( '|', curses.color_pair(self.colors_connection['status_out']) ) else: - if os.geteuid() == 0: - # if there is no connection, display a "None" - self.scr.addstr( 'None', curses.color_pair(self.colors_tunnel['tunnels_nb_none']) ) + # if os.geteuid() == 0: + # if there is no connection, display a "None" + self.scr.addstr( 'None', curses.color_pair(self.colors_tunnel['tunnels_nb_none']) ) self.scr.clrtoeol() @@ -537,11 +555,11 @@ class monitorCurses: # set the color to the highlight one colors = self.colors_highlight - txt = str(self.tm[line][key]) + txt = eval("str(self.tp.get_tunnel(line).%s)" % key) if key == 'target_host' or key == 'via_host': # limit the size of the line to 20 - # TODO avoid hard-coded constants - txt = str(self.tm[line][key]).ljust(20)[:20] + # FIXME avoid hard-coded constants + txt = eval("str(self.tp.get_tunnel(line).%s).ljust(20)[:20]" % key) self.scr.addstr( txt, curses.color_pair(colors[key]) ) self.scr.addstr( '\t', curses.color_pair(colors[key]) ) @@ -650,7 +668,7 @@ if __name__ == "__main__": scr.keypad(1) # create the monitor - mc = monitorCurses( scr ) + mc = CursesMonitor( scr ) # call the monitor mc()