#!/usr/bin/python # # GNOME client for Starnet Data Accounting Systems. # # Copyright (C) 2006 Bernard Blackham # Protocol based on sdalogon.c by Matthew Chapman # # This is a hack that evolved, and is released under the terms of the GNU GPL, # version 2.0 or higher # # # Config files read are /etc/sdalogonrc and ~/.sdalogonrc # The format is: # [sdalogon] # server = # [] # username = # password = # # Multiple sections with different server IPs may appear. # # Setting the server as 'auto' causes the default gateway to be chosen. import pygtk pygtk.require('2.0') import gtk, gobject import pynotify import os, socket, errno, signal import ConfigParser global DEFAULT_SERVER, DEFAULT_USERNAME, DEFAULT_PASSWORD DEFAULT_SERVER = '127.0.0.1' DEFAULT_USERNAME = '' DEFAULT_PASSWORD = '' LONG_NAME = 'SDA Logon Client' SHORT_NAME = 'SDA' CLIENT_VERSION = 'Linux-pySDA-1.0' SDAS_DEFAULT_PORT = 8000 # Seconds between keepalive packets KEEPALIVE_INTERVAL = 60 # If more than this amount (in MB) is downloaded within one KEEPALIVE_INTERVAL # a notification icon appears to remind you of the amount downloaded. DOWNLOAD_1MIN_WARNING_LIMIT = 1 # MB # For converting megabytes to dollars DOLLARS_PER_MEGABYTE = 0.04 # Internal state keeping STATE_DISCONNECTED = 1 STATE_CONNECTING = 2 STATE_CONNECTED = 3 STATE_DISCONNECTING = 4 # Magic protocol constants MODE_LOGON = 1 MODE_LOGOFF = 2 MODE_KEEPALIVE = 3 def read_config(): c = ConfigParser.SafeConfigParser() c.read(['/etc/sdalogonrc', os.path.expanduser('~/.sdalogonrc')]) try: server = c.get('sdalogon', 'server') except: server = 'auto' if server == 'auto': server = guess_sdanet_server() try: username = c.get(server, 'username') except: username = os.environ['USER'] try: password = c.get(server, 'password') except: password = '' global DEFAULT_SERVER, DEFAULT_USERNAME, DEFAULT_PASSWORD DEFAULT_SERVER = server DEFAULT_USERNAME = username DEFAULT_PASSWORD = password def guess_sdanet_server(): # Use our default route to google. f = os.popen('/sbin/ip route get 66.102.7.147') s = f.readline().strip() f.close() bits = s.split(' ') if len(bits) > 2 and bits[1] == 'via': return bits[2] return DEFAULT_SERVER # Unknown! def encode(s): # A truly unbreakable cipher. r = '' i = 0 for c in s: r += chr(ord(c) + i%7) i += 1 return r def decode(s): # Break the unbreakable. r = '' i = 0 for c in s: r += chr(ord(c) - i%7) i += 1 return r class SDAMessage: def __init__(self, serverip, serverport, callback): self.serverip = serverip self.serverport = serverport self.callback = callback self.response = '' def LogOn(self, username, password): self.mode = MODE_LOGON self.username = username self.password = password self.sendit() def KeepAlive(self, username, password): self.mode = MODE_KEEPALIVE self.username = username self.password = password self.sendit() def LogOff(self, username, password): self.mode = MODE_LOGOFF self.username = username self.password = password self.sendit() def sendit(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setblocking(False) try: self.sock.connect((self.serverip, self.serverport)) except socket.error, (e, v): if e != errno.EINPROGRESS: self.callback(None, v) return self.input_watch = gobject.io_add_watch( self.sock, gobject.IO_IN, self.have_input) self.output_watch = gobject.io_add_watch( self.sock, gobject.IO_OUT, self.can_output) self.response = '' def cancel(self): self.cleanup() def cleanup(self): self.sock.close() gobject.source_remove(self.input_watch) gobject.source_remove(self.output_watch) def have_input(self, sock, condition): try: rbuf = sock.recv(1024) except socket.error, (e, v): self.callback(None, v) self.cleanup() return False if len(rbuf) == 0: # EOF self.callback(self.response, None) self.cleanup() return False self.response += decode(rbuf) return True def can_output(self, sock, condition): msg = '%d %s %s %s 0 %s '%( self.mode, self.username, self.password, sock.getsockname()[0], CLIENT_VERSION) print 'Sending ', msg b = sock.send(encode(msg)) # FIXME: assumed that we were able to send it all at once. return False class SDALogon: def error(self, msg): dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg) dialog.run() dialog.destroy() def internal_error(self, msg): error('Internal error: %s') def set_tooltip(self, msg): self.status_icon.set_tooltip(LONG_NAME+'\n'+msg) def set_state(self, state): self.state = state if state == STATE_CONNECTING: self.set_tooltip('Connecting...') self.status_icon.set_from_stock(gtk.STOCK_CONNECT) self.status_icon.set_blinking(True) elif state == STATE_CONNECTED: self.set_tooltip('Connected.') self.status_icon.set_blinking(False) self.status_icon.set_from_stock(gtk.STOCK_CONNECT) elif state == STATE_DISCONNECTED: self.set_tooltip('Disconnected.') self.status_icon.set_blinking(False) self.status_icon.set_from_stock(gtk.STOCK_DISCONNECT) elif state == STATE_DISCONNECTING: self.set_tooltip('Disconnecting...') self.status_icon.set_from_stock(gtk.STOCK_DISCONNECT) self.status_icon.set_blinking(True) def popup_notify(self, msg): if msg == None: return self.notify.set_property('body', msg) (_, area, _) = self.status_icon.get_geometry() self.notify.set_hint('x', area[0]) self.notify.set_hint('y', area[1]) self.notify.show() def display_msg(self, msg, notify=False): if msg == None: self.separator.hide() self.status_text.hide() self.status_text.set_text('') else: self.separator.show() self.status_text.set_text(msg) self.status_text.show() if notify and self.hidden: self.popup_notify(msg) def reset_state(self): self.set_state(STATE_DISCONNECTED) if self.xaction != None: self.xaction.cancel() self.xaction = None self.change_connected_button(False) def parse_quota_info(self, message): bits = message.split(' ') be_noisy = False try: if bits[3] != 'Quota' or bits[5] != 'Used': raise Exception("bugger") start = float(bits[4].strip('Mb;')) used = float(bits[6].strip('Mb;')) remaining = start - used message = 'Used: %.3f MB ($%.2f)\nRemaining: %.3f MB ($%.2f)'%( used, used*DOLLARS_PER_MEGABYTE, remaining, remaining*DOLLARS_PER_MEGABYTE) if self.last_remaining == None or \ self.last_remaining - remaining > DOWNLOAD_1MIN_WARNING_LIMIT: be_noisy = True elif self.last_shown == None or \ self.last_shown - remaining > 1.0: be_noisy = True self.last_remaining = remaining if be_noisy: self.last_shown = remaining except Exception, (e): print "Failed to parse:",e self.display_msg(message, be_noisy) self.set_tooltip(message) def get_response(self, message, error): print "Response", message, error self.xaction = None if self.state == STATE_CONNECTING: if error != None: self.display_msg('Could not connect: '+error) self.reset_state() else: bits = message.split(' ') if len(bits) >= 3 and bits[3] == 'Login_Ok': self.display_msg('Connected.', True) self.set_state(STATE_CONNECTED) self.do_keepalive() else: self.display_msg('Response: '+message, True) self.reset_state() elif self.state == STATE_CONNECTED: if error != None: self.display_msg('Could not send keepalive: '+error, True) else: self.parse_quota_info(message) elif self.state == STATE_DISCONNECTING: if error != None: self.display_msg('Error in log off: '+error, True) self.reset_state() else: self.reset_state() bits = message.split(' ') if len(bits) >= 1 and bits[1] == '1': self.display_msg('Disconnected.\n'+' '.join(bits[2:]), True) self.set_state(STATE_DISCONNECTED) else: self.display_msg('Response: '+message, True) if self.want_to_quit: gtk.main_quit() self.last_shown = None self.last_remaining = None def do_connect(self): if self.state != STATE_DISCONNECTED: self.internal_error('not disconnected') return self.set_state(STATE_CONNECTING) self.xaction = SDAMessage(self.serverip, self.serverport, self.get_response) self.xaction.LogOn(self.username, self.password) def do_keepalive(self): if self.state != STATE_CONNECTED: return True self.xaction = SDAMessage(self.serverip, self.serverport, self.get_response) self.xaction.KeepAlive(self.username, self.password) return True def do_disconnect(self): if self.state == STATE_DISCONNECTING: return self.set_state(STATE_DISCONNECTING) self.xaction = SDAMessage(self.serverip, self.serverport, self.get_response) self.xaction.LogOff(self.username, self.password) def delete_event(self, widget, event, data=None): self.hide(widget, data) return True def connect(self, widget, event, data=None): # State should be STATE_DISCONNECTED self.username = self.username_entry.get_text() self.password = self.password_entry.get_text() bits = self.serverip_entry.get_text().split(':') self.serverip = bits[0] if len(bits) > 2: error('Invalid server specification.') return if len(bits) > 1: self.serverport try: self.serverport = int(bits[2]) except ValueError: error('Invalid port specified (%s)'%(bits[2])) return else: self.serverport = SDAS_DEFAULT_PORT self.change_connected_button(True) self.display_msg('Connecting to %s:%d ...'%(self.serverip, self.serverport), True) self.do_connect() def disconnect(self, widget, event, data=None): # State should be STATE_CONNECTING or STATE_CONNECTED self.display_msg('Disconnecting ...', True) self.do_disconnect() def hide(self, widget, data=None): self.hidden = True self.showhide_menuitem.set_active(False) self.window.hide() def show(self, widget, data=None): self.hidden = False self.showhide_menuitem.set_active(True) self.window.show() def toggle_show(self, widget, data=None): if self.toggle_rec > 0: return self.toggle_rec += 1 if self.hidden: self.show(self.window, data) else: self.hide(self.window, data) self.toggle_rec -= 1 def exit(self, widget, data=None): if self.want_to_quit: # Quit if the user presses it again. gtk.main_quit() if self.state == STATE_CONNECTING: if self.xaction != None: self.xaction.cancel() self.disconnect(None, None) self.want_to_quit = True elif self.state == STATE_CONNECTED: self.disconnect(None, None) self.want_to_quit = True elif self.state == STATE_DISCONNECTING: self.want_to_quit = True else: gtk.main_quit() def change_connected_button(self, state): if state == True: self.connect_button.hide() self.connect_menuitem.set_sensitive(False) self.disconnect_button.show() self.disconnect_menuitem.set_sensitive(True) else: self.connect_button.show() self.connect_menuitem.set_sensitive(True) self.disconnect_button.hide() self.disconnect_menuitem.set_sensitive(False) def popup_menu(self, widget, button, time, data=None): self.menu.popup(None, None, None, button, time) def create_window(self): self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title(LONG_NAME) self.window.connect("delete_event", self.delete_event) self.window.connect("destroy", self.hide) self.window.set_border_width(20) vbox = gtk.VBox(False, 20) self.window.add(vbox) frame = gtk.Frame('Login Details') vbox.add(frame) table = gtk.Table(3, 2, False) frame.add(table) table.attach(gtk.Label('Server IP'), 0, 1, 0, 1, xpadding=10) self.serverip_entry = gtk.Entry() self.serverip_entry.set_activates_default(True) self.serverip_entry.set_text(DEFAULT_SERVER) table.attach(self.serverip_entry, 1, 2, 0, 1, xpadding=10) table.attach(gtk.Label('Username'), 0, 1, 1, 2, xpadding=10) self.username_entry = gtk.Entry() self.username_entry.set_activates_default(True) self.username_entry.set_text(DEFAULT_USERNAME) table.attach(self.username_entry, 1, 2, 1, 2, xpadding=10) table.attach(gtk.Label('Password'), 0, 1, 2, 3, xpadding=10) self.password_entry = gtk.Entry() self.password_entry.set_activates_default(True) self.password_entry.set_visibility(False) self.password_entry.set_text(DEFAULT_PASSWORD) table.attach(self.password_entry, 1, 2, 2, 3, xpadding=10) button_box = gtk.HButtonBox() button_box.set_layout(gtk.BUTTONBOX_SPREAD) vbox.add(button_box) self.connect_button = gtk.Button(stock=gtk.STOCK_CONNECT) self.connect_button.connect('clicked', self.connect, None) self.connect_button.set_flags(gtk.CAN_DEFAULT) button_box.add(self.connect_button) self.disconnect_button = gtk.Button(stock=gtk.STOCK_DISCONNECT) self.disconnect_button.connect('clicked', self.disconnect, None) button_box.add(self.disconnect_button) quit_button = gtk.Button(stock=gtk.STOCK_QUIT) quit_button.connect('clicked', self.exit, None) button_box.add(quit_button) self.menu = gtk.Menu() self.connect_menuitem = gtk.ImageMenuItem(gtk.STOCK_CONNECT) self.connect_menuitem.connect('activate', self.connect, None) self.disconnect_menuitem = gtk.ImageMenuItem(gtk.STOCK_DISCONNECT) self.disconnect_menuitem.connect('activate', self.disconnect, None) self.showhide_menuitem = gtk.CheckMenuItem('_Show Window') self.showhide_menuitem.set_active(True) self.showhide_menuitem.connect('toggled', self.toggle_show, True) quit_menuitem = gtk.ImageMenuItem(gtk.STOCK_QUIT) quit_menuitem.connect('activate', self.exit, None) self.menu.append(self.connect_menuitem) self.menu.append(self.disconnect_menuitem) self.menu.append(gtk.SeparatorMenuItem()) self.menu.append(self.showhide_menuitem) self.menu.append(quit_menuitem) self.menu.show_all() self.separator = gtk.HSeparator() vbox.add(self.separator) self.status_text = gtk.Label() vbox.add(self.status_text) self.window.set_default(self.connect_button) self.window.set_resizable(False) self.window.show_all() self.display_msg(None) self.change_connected_button(False) self.status_icon = gtk.StatusIcon() self.status_icon.set_from_stock(gtk.STOCK_DISCONNECT) self.status_icon.set_visible(True) self.status_icon.connect('activate', self.toggle_show, None) self.status_icon.connect('popup-menu', self.popup_menu, None) pynotify.init(LONG_NAME) self.notify = pynotify.Notification(LONG_NAME) self.notify.set_timeout(5000) self.want_to_quit = False def __init__(self): self.username = '' self.password = '' self.serverip = '' self.serverport = SDAS_DEFAULT_PORT self.state = STATE_DISCONNECTED self.last_shown = None self.last_remaining = None self.hidden = False self.toggle_rec = 0 self.create_window() gobject.timeout_add(KEEPALIVE_INTERVAL*1000, self.do_keepalive) def die_signal(self, signum, frame): self.exit(None) def run(self): signal.signal(signal.SIGINT, self.die_signal) signal.signal(signal.SIGTERM, self.die_signal) signal.signal(signal.SIGHUP, signal.SIG_IGN) gtk.main() if __name__ == '__main__': read_config() SDALogon().run()