#! /usr/bin/python -E

#
# Copyright (C) 2006 Red Hat, Inc.
#
# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
#

PROGNAME="setroubleshoot"

import gettext
gettext.bindtextdomain(PROGNAME, "/usr/share/locale")
gettext.textdomain(PROGNAME)
try:
    gettext.install(PROGNAME, localedir="/usr/share/locale", unicode=1)
except IOError:
    import __builtin__
    __builtin__.__dict__['_'] = unicode


import dbus
import dbus.glib
import dbus.service
import errno
import gobject
import os
import Queue
import re
import signal
import socket as Socket
import sys

from setroubleshoot.analyze import *
from setroubleshoot.config import cfg
from setroubleshoot.errcode import *
from setroubleshoot.log import *
from setroubleshoot.signature import *
from setroubleshoot.util import *
from setroubleshoot.rpc import *
from setroubleshoot.rpc_interfaces import *

#------------------------------------------------------------------------------
status_icon = None
dbus_bus_name = cfg.get('session_dbus','bus_name')
# FIXME: why isn't this read from config file?
dbus_object_path = "/com/redhat/selinux/alert_object"
app = None
#------------------------------------------------------------------------------
def sighandler(signum, frame):
    if debug:
        log_program.debug("exiting on signal %s", signum)
    sys.exit()

def setup_sighandlers():
    signal.signal(signal.SIGHUP,  sighandler)
    signal.signal(signal.SIGQUIT, sighandler)
    signal.signal(signal.SIGTERM, sighandler)

def run_app():
    global app

    app = SEAlert()
    return app.main()    

def run_as_dbus_service():
    global app

    try:
        if debug:
            log_dbus.debug('starting service')
        dbus_service = DBusService(dbus_bus_name)
        app = SEAlert(dbus_service.presentation_manager)
        return app.main()    
    except dbus.DBusException, e:
        log_dbus.error('could not start dbus: %s', str(e))
        return False

def ask_dbus_to_show_browser():
    try:
        bus=dbus.SessionBus()
        proxy_obj=bus.get_object(dbus_bus_name, dbus_object_path)
        iface=dbus.Interface(proxy_obj, 'com.redhat.SEtroubleshootIface')
        iface.show_browser()    
        return True
    except dbus.DBusException, e:
        log_dbus.error('could not start dbus: %s', str(e))
        return False

def ask_dbus_to_quit_app():
    try:
        bus=dbus.SessionBus()
        proxy_obj=bus.get_object(dbus_bus_name, dbus_object_path)
        iface=dbus.Interface(proxy_obj, 'com.redhat.SEtroubleshootIface')
        iface.quit_app()    
        return True
    except dbus.DBusException, e:
        log_dbus.error('could not start dbus: %s', str(e))
        return False

def command_line_lookup_id(local_id, html=False):
    def lookup_local_id():
        if debug:
            log_rpc.debug("calling server to lookup id (%s)", local_id)
        async_rpc = cl.alert_client.lookup_local_id(local_id)
        async_rpc.add_callback(lookup_local_id_callback)
        async_rpc.add_errback(lookup_local_id_error)

    def lookup_local_id_callback(siginfo):
        if html:
            print siginfo.format_html()
        else:
            print siginfo.format_text()
        cl.main_loop.quit()

    def lookup_local_id_error(method, errno, strerror):
        print "%s error (%d): %s" % (method, errno, strerror)
        cl.main_loop.quit()


    cl = SECommandLine(lookup_local_id)
    cl.run()

#-----------------------------------------------------------------------------

class PresentationManager(gobject.GObject):
    __gsignals__ = {
        'show_browser':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'quit_app':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        }

    def __init__(self):
        gobject.GObject.__init__(self)

    def show_browser(self, target = None):
        self.emit('show_browser', target)

    def quit_app(self):
        self.emit('quit_app')

gobject.type_register(PresentationManager)

#-----------------------------------------------------------------------------

class DBusService(dbus.service.Object):
    def __init__(self, bus_name):
        bus = dbus.SessionBus()
        bus_name = dbus.service.BusName(dbus_bus_name, bus = bus)
        dbus.service.Object.__init__(self, bus_name, dbus_object_path)

        self.presentation_manager = PresentationManager()

    @dbus.service.method("com.redhat.SEtroubleshootIface")
    def start(self):
        return _("Started")

    @dbus.service.method("com.redhat.SEtroubleshootIface")
    def show_browser(self):
        if debug:
            log_dbus.debug('dbus iface show_browser() called',)
        self.presentation_manager.show_browser()
        return ""

    @dbus.service.method("com.redhat.SEtroubleshootIface")
    def quit_app(self):
        if debug:
            log_dbus.debug('quit_app() called')
        self.presentation_manager.quit_app()


#------------------------------------------------------------------------------
class StatusIcon(gobject.GObject):
    __gsignals__ = {
        'show_browser':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        }

    def __init__(self):
        gobject.GObject.__init__(self)

        status_icon_file = cfg.get('alert','status_icon')
        self.status_icon = gtk.status_icon_new_from_file(status_icon_file)
        self.status_icon.set_visible(False)
        self.status_icon.set_blinking(False)
        self.status_icon.set_tooltip(_("SELinux AVC denial, click to view"))
        self.status_icon.connect("activate", self.browser_activate)

        pynotify.init("setroubleshoot")

    def calculate_notify_position(self, notify):
        self.get_icon_geometry()
        if self.orientation == gtk.ORIENTATION_HORIZONTAL:
            if self.icon_rect.y < self.screen_height / 2:
                # Panel Top Edge
                notify_x = self.icon_rect.x + self.icon_size/2
                notify_y = self.icon_rect.y + self.icon_size/2
            else:
                # Panel Bottom Edge
                notify_x = self.icon_rect.x + self.icon_size/2
                notify_y = self.icon_rect.y - self.icon_size/2
        elif self.orientation == gtk.ORIENTATION_VERTICAL:
            if self.icon_rect.x < self.screen_width / 2:
                # Panel Left Edge
                notify_x = self.icon_rect.x + self.icon_size/2
                notify_y = self.icon_rect.y + self.icon_size/2
            else:
                # Panel Right Edge
                notify_x = self.icon_rect.x - self.icon_size/2
                notify_y = self.icon_rect.y + self.icon_size/2
        else:
            raise ValueError("unknown orientation = %d" % self.orientation)

        notify.set_hint("x", notify_x)
        notify.set_hint("y", notify_y)

    def get_icon_geometry(self):
        (self.screen, self.icon_rect, self.orientation) = \
                      self.status_icon.get_geometry()
        self.icon_size = self.status_icon.get_size()
        self.screen_width = self.screen.get_width()
        self.screen_height = self.screen.get_height()

        #print "rect=(%dx%d)(%d,%d) size=%d embedded=%s %s screen=(%dx%d)"  % \
        #      (self.icon_rect.width, self.icon_rect.height,
        #       self.icon_rect.x, self.icon_rect.y, self.icon_size,
        #       self.status_icon.is_embedded(),
        #       orientation_str[self.orientation],
        #       self.screen_width, self.screen_height)

    def display_notification(self):
        self.notify = pynotify.Notification(_("SELinux"),
                                            _("AVC denial, click icon to view"))
        self.calculate_notify_position(self.notify)
        self.notify.show()
        return False

    def display_new_alert_is_pending(self):
        if debug:
            log_alert.debug("display_new_alert_is_pending()")
        if self.status_icon.get_visible():
            return
        self.status_icon.set_visible(True)

        # HACK WARNING: The information returned from the status icon's
        # get_geometry() call is only valid at certain moments in
        # time, e.g. while it's displayed on the screen. When we set
        # its visibility to True that only queues a request for it to
        # be made visible, calling get_geometry after setting its
        # visibility to true is too soon for the position information
        # to be valid. Unforunately there is no signal emitted when
        # the position information is valid or changes. The most
        # reliable thing we can do for the time being is queue a
        # callback for a short duration in the future to display the
        # notification and hope the icon is visible and its display
        # informaiton is valid at the moment :-(

        if cfg.getboolean('alert','use_notification'):
            gobject.timeout_add(200, self.display_notification)
            
    def browser_activate(self, status_icon, data = None):
        self.status_icon.set_visible(False)
        self.notify.close()
        self.emit('show_browser', 'audit')

gobject.type_register(StatusIcon)

#-----------------------------------------------------------------------------

class ServerConnectionHandler(RpcChannel,
                              SETroubleshootServerInterface,
                              SETroubleshootDatabaseInterface,
                              gobject.GObject):
    __gsignals__ = {
        'alert':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'connection_state_change':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'connection_pending_retry':
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
        'signatures_updated': 
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
        'database_bind': 
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)),
        }

    def __init__(self, username):
        RpcChannel.__init__(self, self.on_connection_state_change, channel_type = 'sealert')
        gobject.GObject.__init__(self)

        self.connect_rpc_interface('SEAlert', self)
        self.connect_rpc_interface('SETroubleshootDatabaseNotify', self)

        self.pkg_version = cfg.get('general','pkg_version')
        self.rpc_version = cfg.get('general','rpc_version')
        self.username = username
        self.retry_attemps = 0
        self.seconds_pending_till_connect_retry = 0
        self.report_connect_failure = True
        self.database_name = 'audit_listener'

    def bind(self):
        def database_bind_callback(properties):
            if debug:
                log_rpc.debug('database_bind_callback properties = %s', str(properties))
            self.emit('database_bind', self, properties)
            

        def database_bind_error(method, errno, strerror):
            if debug:
                log_rpc.error('database bind: %s', strerror)

        async_rpc = self.database_bind(self.database_name)
        async_rpc.add_callback(database_bind_callback)
        async_rpc.add_errback(database_bind_error)

    def on_connection_state_change(self, connection_state):
        if debug:
            log_rpc.debug("on_connection_state_change: state = %s channel = %s, type = %s",
                          connection_state, self.channel_name, self.channel_type)

        self.emit('connection_state_change', connection_state)

        if connection_state == 'closed':
            self.open_with_retry()
        elif connection_state == 'open':
            self.bind()
        elif connection_state == 'retry':
            pass
        elif connection_state == 'hup':
            pass
        elif connection_state == 'error':
            pass
        elif connection_state == 'invalid':
            pass
        elif connection_state == 'timeout':
            pass
        else:
            log_rpc.error("on_connection_state_change: unknown state (%s) channel = %s type = %s",
                          connection_state, self.channel_name, self.channel_type)

    def get_connection_state(self):
        return self.connection_state

    def open(self, socket_address = None):
        if socket_address is not None:
            self.socket_address = socket_address
            self.address_family, self.socket_type = get_socket_family_and_type(socket_address)
        if self.connection_state == 'open':
            return True
        try:
            self.socket = Socket.socket(self.address_family, self.socket_type)
            self.socket.connect(self.socket_address)
            self.io_watch_id = gobject.io_add_watch(self.socket, io_input_conditions, self.handle_client_io)
            self.retry_attemps = 0
            self.set_connection_state('open')
            self.report_connect_failure = True
            self.do_logon()
        except Socket.error, e:
            if self.report_connect_failure == True:
                log_rpc.error("attempt to open server connection failed: %s", e)
                self.report_connect_failure = False

                
            self.set_connection_state('error')
            return False
        return True
            
    def get_retry_interval(self):
        if self.retry_attemps < 5:
            return 5
        else:
            return 60

    def retry_countdown(self):
        self.seconds_pending_till_connect_retry -= 1
        if self.seconds_pending_till_connect_retry <= 0:
            self.open_with_retry()
            return False
        else:
            self.emit('connection_pending_retry', self.seconds_pending_till_connect_retry)
            return True

    def open_with_retry(self, socket_address = None):
        self.emit('connection_pending_retry', 0)
        if not self.open(socket_address):
            self.set_connection_state('retry')
            self.retry_attemps += 1
            self.seconds_pending_till_connect_retry = self.get_retry_interval()
            gobject.timeout_add(1*1000, self.retry_countdown)
            return False
        return False
        
    def evaluate_server_version(self, pkg_version, rpc_version):
        if pkg_version != self.pkg_version:
            if debug:
                log_program.debug("restarting client because server pkg_version(%s) != client pkg_version(%s)",
                                  pkg_version, self.pkg_version)

            if not display_restart(30, do_restart):
                do_restart()

    def do_logon(self):
        def logon_callback(pkg_version, rpc_version):
            if debug:
                log_program.debug("logon_callback(): pkg_version=%s rpc_version=%s", pkg_version, rpc_version)
            self.evaluate_server_version(pkg_version, rpc_version)


        def logon_error(method, errno, strerror):
            log_program.error("%s: %s", method, strerror)

        self.channel_name = self.username
        async_rpc = self.logon(self.channel_type, self.username, 'passwd')
        async_rpc.add_callback(logon_callback)
        async_rpc.add_errback(logon_error)

    # ------

    def alert(self, siginfo):
        if debug:
            log_alert.debug("received alert")
        self.emit('alert', siginfo)

    def signatures_updated(self, type, item):
        if debug:
            log_rpc.debug('signatures_updated() alert client: type=%s item=%s', type, item)
        self.emit('signatures_updated', type, item)
        
gobject.type_register(ServerConnectionHandler)

#-----------------------------------------------------------------------------

class SEAlert(object):
    """
    The SEAlert object represents a gui client for setroubleshoot. It
    processes alerts and presents the user with an appropriate user
    interface for handling the alert. Most of the interface code
    is in BrowserApplet and StatusIcon. This class is mainly a central
    hub for processing the alerts.
    """
    def __init__(self, presentation_manager = None):
        try:
            self.username = get_identity()

            if get_display() is None:
                print >> sys.stderr, "cannot open X display, exiting ..."
                sys.exit(1)
            from setroubleshoot.browser import BrowserApplet

            if presentation_manager is None:
                self.presentation_manager = PresentationManager()
                gobject.idle_add(self.show_browser_at_startup)
            else:
                self.presentation_manager = presentation_manager

            self.browser = None

            self.status_icon = StatusIcon()
            self.status_icon.connect('show_browser', self.on_show_browser)


            self.alert_client = ServerConnectionHandler(self.username)
            self.alert_client.open_with_retry(get_server_address('local_fault_server'))

            self.browser = BrowserApplet(self.username, self.alert_client)
            self.presentation_manager.connect('show_browser', self.on_show_browser)
            self.presentation_manager.connect('quit_app', self.on_quit)

            self.alert_client.connect('alert', self.alert)

            # If there is no presentation mananger make sure when the
            # user closes the window the whole application exits. When running
            # in "alert" mode we want the application to persist in the background
            if presentation_manager is None:
                self.browser.window_delete_hides = False

        except ProgramError, e:
            log_program.error(e.strerror)
            log_program.exception(e.strerror)
            sys.exit(1)

    def main(self):
        if debug:
            log_program.debug('creating main GUI application')
        try:
            gtk.main()
        except KeyboardInterrupt, e:
            sys.exit()

    def alert(self, alert_client, siginfo):
        if debug:
            log_alert.debug("evaluating alert")
        def alert_filter_result(result):
            if result == 'display':
                self.status_icon.display_new_alert_is_pending()

        self.browser.server.evaluate_alert_filter(
            siginfo.sig, self.username
            ).add_callback(alert_filter_result)

    def show_browser_at_startup(self):
        self.presentation_manager.show_browser()
        return False

    def show_browser(self, target):
        if target is not None:
            self.browser.update_visit(target)
        if debug:
            log_gui.debug("SEAlert.show_browser(): target=%s", target)
        self.browser.show()
        return True

    def on_quit(self, widget):
        gtk.main_quit()

    def on_show_browser(self, widget, target):
        self.show_browser(target)

def do_restart():
    window_state = None
    geometry = None
    if app is not None:
        if app.browser is not None:
            window_state = app.browser.get_window_state()
            geometry = app.browser.get_geometry()
            os.environ['SEALERT_WINDOW_STATE'] = window_state
            os.environ['SEALERT_WINDOW_GEOMETRY'] = geometry
    log_program.info("restarting %s: args=%s window_state=%s geometry=%s",
                     sys.argv[0], sys.argv[1:], window_state, geometry)
    os.execv(sys.argv[0], sys.argv)

#-----------------------------------------------------------------------------

class SECommandLine(object):
    def __init__(self, func):
        self.username = get_identity()
        self.func = func

        self.alert_client = ServerConnectionHandler(self.username)
        self.alert_client.connect('connection_state_change', self.on_connection_state_change)
        self.main_loop = gobject.MainLoop()


    def on_connection_state_change(self, alert_client, connection_state):
        if debug:
            log_program.debug("on_connection_state_change: state = %s", connection_state)

        if connection_state == 'closed':
            pass
        elif connection_state == 'open':
            self.func()
        elif connection_state == 'retry':
            pass
        elif connection_state == 'hup':
            pass
        elif connection_state == 'error':
            print >> sys.stderr, "failed to connect to server"
            sys.exit(1)
        elif connection_state == 'invalid':
            pass
        elif connection_state == 'timeout':
            pass
        else:
            log_rpc.error("on_connection_state_change: unknown state (%s) channel = %s type = %s",
                          connection_state, self.channel_name, self.channel_type)

    def run(self):
        if debug:
            log_program.debug('executing command line application')
        self.alert_client.open(get_server_address('local_fault_server'))
        try:
            self.main_loop.run()
        except KeyboardInterrupt, e:
            sys.exit()

#-----------------------------------------------------------------------------

class SECommandLineLocal(object):
    def __init__(self, func):
        self.username = get_identity()
        self.func = func

        self.main_loop = gobject.MainLoop()


    def run(self):
        if debug:
            log_program.debug('executing command line application')
        try:
            gobject.idle_add(self.func)
            self.main_loop.run()
        except KeyboardInterrupt, e:
            sys.exit()

#-----------------------------------------------------------------------------

def do_analyze_logfile(logfile_path, html):
    def progress_callback(progress):
        output = "\r%3d%% done" % (progress*100)
        sys.stdout.write(output)
        sys.stdout.flush()

    if sys.stdout.isatty():
        progress = progress_callback
    else:
        progress = None

    try:
        analyzer = LogfileAnalyzer(logfile_path)
        database = analyzer.database

        sigs = analyzer.analyze_logfile(progress)
        if progress:
            sys.stdout.write('\n')
            sys.stdout.flush()

        sigs = database.query_alerts('*')

        seperator = '-'*80 + '\n'
        if not html:
            print "found %d alerts in %s" % (len(sigs.signature_list), logfile_path)
        for siginfo in sigs.signature_list:
            if html:
                print siginfo.format_html()
            else:
                print seperator
                print siginfo.format_text()


        # FIXME
        #logfile_database.remove()
    except ProgramError, e:
        print >> sys.stderr, e.strerror
        
        
#-----------------------------------------------------------------------------

# -- Main --
if __name__ == '__main__':
    gobject.threads_init()
    setup_sighandlers()
        
    if debug:
        log_program.debug("main() args=%s", sys.argv)

    def usage():
        print _('''
        -b --browser        Launch the browser
        -h --help           Show this message
        -s --service        Start sealert as a dbus service
        -S --noservice      Start sealert without dbus service as stand alone app
        -l --lookupid id    Lookup alert by id	
        -a --analyze file   Scan a log file, analyze it's AVC's
        -H --html_output    Ouput in html
        -v --verbose        Start in verbose mode
        ''')

    try:
        import getopt

        try:
            opts, args = getopt.getopt(sys.argv[1:], 'bhHqsSl:a:v',
                         ['help', 'html', 'browser','quit','service','noservice','lookupid = ',
                          'analyze = ','verbose'])
        except getopt.GetoptError:
            # print help information and exit:
            usage()
            sys.exit(2)

        verbose = False
        html = False
        browser = False
        lookup = False
        analyze = False
        typeind = 0

        for o, a in opts:
            if o in ('-h', '--help'):
                usage()
                sys.exit()

            if o in ('-b', '--browser'):
                typeind += 1
                browser = True
                if debug:
                    log_dbus.debug("cmdline arg -b, calling ask_dbus_to_show_browser()")
            if o in ('-q', '--quit'):
                if debug:
                    log_dbus.debug("cmdline arg -q, calling ask_dbus_to_quit_app()")
                if not ask_dbus_to_quit_app():
                    print >> sys.stderr, "could not attach to desktop process"
                sys.exit()

            if o in ('-s', '--service'):
                if debug:
                    log_dbus.debug("cmdline arg -s, calling run_as_dbus_service()")
                # This import must come before importing gtk to silence warnings
                from setroubleshoot.gui_utils import *
                import gtk
                import pynotify
                from setroubleshoot.runcmd import *
                run_as_dbus_service()
                sys.exit()

            if o in ('-S', '--noservice'):
                if debug:
                    log_program.debug("cmdline arg -S, calling run_app()")
                # This import must come before importing gtk to silence warnings
                from setroubleshoot.gui_utils import *
                import gtk
                import pynotify
                from setroubleshoot.runcmd import *
                run_app()
                sys.exit()

            if o in ('-l', '--lookupid'):
                typeind += 1
                lookup = True
                local_id = a
                if debug:
                    log_program.debug("cmdline arg -l, calling command_line_lookup_id(%s)", local_id)

            if o in ('-H', '--html'):
                html = True

            if o in ('-a', '--analyze'):
                typeind += 1
                analyze = True
                logfile = a
                if debug:
                    log_program.debug("cmdline arg -a, calling analyze_logfile(%s)", logfile)
            if o in ('-v', '--verbose'):
                verbose = True


        # Attempt to communicate with the service.  DBus should start it if it is not
        # running, otherwise we will become the service
        if typeind == 0:
            try:
                bus = dbus.SessionBus()
                proxy_obj = bus.get_object(dbus_bus_name, dbus_object_path)
                iface = dbus.Interface(proxy_obj, 'com.redhat.SEtroubleshootIface')
                s = iface.start()
            except dbus.DBusException:
                print >> sys.stderr, "could not attach to desktop process"
                pass
            sys.exit()

        if typeind > 1:
            print >> sys.stderr, "Only select one of browser, lookup or analyze"
            usage()
            sys.exit(3)


        if browser and not ask_dbus_to_show_browser():
            print >> sys.stderr, "could not attach to desktop process, running standalone"
            sys.exit()

        if lookup:
            command_line_lookup_id(local_id, html)
            sys.exit()

        if analyze:
            do_analyze_logfile(logfile, html)
            sys.exit()

    except SystemExit, e:
        pass
    except Exception, e:
        display_traceback('sealert')
