Ejemplo n.º 1
0
    def __init__(self, parent=None, appicon=None):
        QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)

        self._themes = Themes.instance()
        self._saved_theme = ""

        self._cfg = Config.get()
        self._nodes = Nodes.instance()
        self._db = Database.instance()

        self._notification_callback.connect(self._cb_notification_callback)
        self._notifications_sent = {}
        self._desktop_notifications = DesktopNotifications()

        self.setupUi(self)
        self.setWindowIcon(appicon)

        self.dbFileButton.setVisible(False)
        self.dbLabel.setVisible(False)
        self.dbType = None

        self.acceptButton.clicked.connect(self._cb_accept_button_clicked)
        self.applyButton.clicked.connect(self._cb_apply_button_clicked)
        self.cancelButton.clicked.connect(self._cb_cancel_button_clicked)
        self.helpButton.clicked.connect(self._cb_help_button_clicked)
        self.popupsCheck.clicked.connect(self._cb_popups_check_toggled)
        self.dbFileButton.clicked.connect(self._cb_file_db_clicked)
        self.checkUIRules.toggled.connect(self._cb_check_ui_rules_toggled)
        self.cmdTimeoutUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.SUM))
        self.cmdTimeoutDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.REST))
        self.cmdDBMaxDaysUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.SUM))
        self.cmdDBMaxDaysDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.REST))
        self.cmdDBPurgesUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.SUM))
        self.cmdDBPurgesDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.REST))
        self.cmdTestNotifs.clicked.connect(self._cb_test_notifs_clicked)
        self.radioSysNotifs.clicked.connect(self._cb_radio_system_notifications)
        self.helpButton.setToolTipDuration(30 * 1000)

        if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
            self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
            self.cancelButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton")))
            self.acceptButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogSaveButton")))
            self.dbFileButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DirOpenIcon")))

        if QtGui.QIcon.hasThemeIcon("list-add") == False:
            self.cmdTimeoutUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdTimeoutDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))
            self.cmdDBMaxDaysUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdDBMaxDaysDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))
            self.cmdDBPurgesUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdDBPurgesDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))
Ejemplo n.º 2
0
class PreferencesDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):

    LOG_TAG = "[Preferences] "
    _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
    saved = QtCore.pyqtSignal()

    TAB_POPUPS = 0
    TAB_UI = 1
    TAB_NODES = 2
    TAB_DB = 3

    SUM = 1
    REST = 0

    def __init__(self, parent=None, appicon=None):
        QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)

        self._cfg = Config.get()
        self._nodes = Nodes.instance()
        self._db = Database.instance()

        self._notification_callback.connect(self._cb_notification_callback)
        self._notifications_sent = {}
        self._desktop_notifications = DesktopNotifications()

        self.setupUi(self)
        self.setWindowIcon(appicon)

        self.dbFileButton.setVisible(False)
        self.dbLabel.setVisible(False)
        self.dbType = None

        self.acceptButton.clicked.connect(self._cb_accept_button_clicked)
        self.applyButton.clicked.connect(self._cb_apply_button_clicked)
        self.cancelButton.clicked.connect(self._cb_cancel_button_clicked)
        self.helpButton.clicked.connect(self._cb_help_button_clicked)
        self.popupsCheck.clicked.connect(self._cb_popups_check_toggled)
        self.dbFileButton.clicked.connect(self._cb_file_db_clicked)
        self.checkUIRules.toggled.connect(self._cb_check_ui_rules_toggled)
        self.cmdTimeoutUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.SUM))
        self.cmdTimeoutDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.REST))
        self.cmdDBMaxDaysUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.SUM))
        self.cmdDBMaxDaysDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.REST))
        self.cmdDBPurgesUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.SUM))
        self.cmdDBPurgesDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.REST))
        self.cmdTestNotifs.clicked.connect(self._cb_test_notifs_clicked)
        self.radioSysNotifs.clicked.connect(self._cb_radio_system_notifications)
        self.helpButton.setToolTipDuration(30 * 1000)

        if QtGui.QIcon.hasThemeIcon("emblem-default") == False:
            self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton")))
            self.cancelButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton")))
            self.acceptButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogSaveButton")))
            self.dbFileButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DirOpenIcon")))

        if QtGui.QIcon.hasThemeIcon("list-add") == False:
            self.cmdTimeoutUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdTimeoutDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))
            self.cmdDBMaxDaysUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdDBMaxDaysDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))
            self.cmdDBPurgesUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp")))
            self.cmdDBPurgesDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown")))

    def showEvent(self, event):
        super(PreferencesDialog, self).showEvent(event)

        try:
            self._settingsSaved = False
            self._reset_status_message()
            self._hide_status_label()
            self.comboNodes.clear()

            self._node_list = self._nodes.get()
            for addr in self._node_list:
                self.comboNodes.addItem(addr)

            if len(self._node_list) == 0:
                self._reset_node_settings()
        except Exception as e:
            print(self.LOG_TAG + "exception loading nodes", e)

        self._load_settings()

        # connect the signals after loading settings, to avoid firing
        # the signals
        self.comboNodes.currentIndexChanged.connect(self._cb_node_combo_changed)
        self.comboNodeAction.currentIndexChanged.connect(self._cb_node_needs_update)
        self.comboNodeDuration.currentIndexChanged.connect(self._cb_node_needs_update)
        self.comboNodeMonitorMethod.currentIndexChanged.connect(self._cb_node_needs_update)
        self.comboNodeLogLevel.currentIndexChanged.connect(self._cb_node_needs_update)
        self.comboNodeLogFile.currentIndexChanged.connect(self._cb_node_needs_update)
        self.comboNodeAddress.currentTextChanged.connect(self._cb_node_needs_update)
        self.checkInterceptUnknown.clicked.connect(self._cb_node_needs_update)
        self.checkApplyToNodes.clicked.connect(self._cb_node_needs_update)
        self.comboDBType.currentIndexChanged.connect(self._cb_db_type_changed)
        self.checkDBMaxDays.toggled.connect(self._cb_db_max_days_toggled)

        # True when any node option changes
        self._node_needs_update = False

    def _load_settings(self):
        self._default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY)
        self._default_target = self._cfg.getSettings(self._cfg.DEFAULT_TARGET_KEY)
        self._default_timeout = self._cfg.getSettings(self._cfg.DEFAULT_TIMEOUT_KEY)
        self._disable_popups = self._cfg.getBool(self._cfg.DEFAULT_DISABLE_POPUPS)

        if self._cfg.hasKey(self._cfg.DEFAULT_DURATION_KEY):
            self._default_duration = self._cfg.getInt(self._cfg.DEFAULT_DURATION_KEY)
        else:
            self._default_duration = self._cfg.DEFAULT_DURATION_IDX

        self.comboUIDuration.setCurrentIndex(self._default_duration)
        self.comboUIDialogPos.setCurrentIndex(self._cfg.getInt(self._cfg.DEFAULT_POPUP_POSITION))

        self.comboUIRules.setCurrentIndex(self._cfg.getInt(self._cfg.DEFAULT_IGNORE_TEMPORARY_RULES))
        self.checkUIRules.setChecked(self._cfg.getBool(self._cfg.DEFAULT_IGNORE_RULES))
        self.comboUIRules.setEnabled(self._cfg.getBool(self._cfg.DEFAULT_IGNORE_RULES))
        #self._set_rules_duration_filter()

        self._cfg.setRulesDurationFilter(
            self._cfg.getBool(self._cfg.DEFAULT_IGNORE_RULES),
            self._cfg.getInt(self._cfg.DEFAULT_IGNORE_TEMPORARY_RULES)
        )

        self.comboUIAction.setCurrentIndex(self._default_action)
        self.comboUITarget.setCurrentIndex(int(self._default_target))
        self.spinUITimeout.setValue(int(self._default_timeout))
        self.spinUITimeout.setEnabled(not self._disable_popups)
        self.popupsCheck.setChecked(self._disable_popups)

        self.showAdvancedCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED))
        self.dstIPCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED_DSTIP))
        self.dstPortCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED_DSTPORT))
        self.uidCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED_UID))

        # by default, if no configuration exists, enable notifications.
        self.groupNotifs.setChecked(self._cfg.getBool(Config.NOTIFICATIONS_ENABLED, True))
        self.radioSysNotifs.setChecked(
            True if self._cfg.getInt(Config.NOTIFICATIONS_TYPE) == Config.NOTIFICATION_TYPE_SYSTEM and self._desktop_notifications.is_available() == True else False
        )
        self.radioQtNotifs.setChecked(
            True if self._cfg.getInt(Config.NOTIFICATIONS_TYPE) == Config.NOTIFICATION_TYPE_QT or self._desktop_notifications.is_available() == False else False
        )

        self.dbType = self._cfg.getInt(self._cfg.DEFAULT_DB_TYPE_KEY)
        self.comboDBType.setCurrentIndex(self.dbType)
        if self.comboDBType.currentIndex() != Database.DB_TYPE_MEMORY:
            self.dbFileButton.setVisible(True)
            self.dbLabel.setVisible(True)
            self.dbLabel.setText(self._cfg.getSettings(self._cfg.DEFAULT_DB_FILE_KEY))
        dbMaxDays = self._cfg.getInt(self._cfg.DEFAULT_DB_MAX_DAYS, 1)
        dbPurgeInterval = self._cfg.getInt(self._cfg.DEFAULT_DB_PURGE_INTERVAL, 5)
        self._enable_db_cleaner_options(self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST), dbMaxDays)
        self.spinDBMaxDays.setValue(dbMaxDays)
        self.spinDBPurgeInterval.setValue(dbPurgeInterval)

        self._load_node_settings()
        self._load_ui_columns_config()

    def _load_node_settings(self):
        addr = self.comboNodes.currentText()
        if addr != "":
            try:
                node_data = self._node_list[addr]['data']
                self.labelNodeVersion.setText(node_data.version)
                self.labelNodeName.setText(node_data.name)
                self.comboNodeLogLevel.setCurrentIndex(node_data.logLevel)

                node_config = json.loads(node_data.config)
                self.comboNodeAction.setCurrentText(node_config['DefaultAction'])
                self.comboNodeDuration.setCurrentText(node_config['DefaultDuration'])
                self.comboNodeMonitorMethod.setCurrentText(node_config['ProcMonitorMethod'])
                self.checkInterceptUnknown.setChecked(node_config['InterceptUnknown'])
                self.comboNodeLogLevel.setCurrentIndex(int(node_config['LogLevel']))

                if node_config.get('Server') != None:
                    self.comboNodeAddress.setEnabled(True)
                    self.comboNodeLogFile.setEnabled(True)

                    self.comboNodeAddress.setCurrentText(node_config['Server']['Address'])
                    self.comboNodeLogFile.setCurrentText(node_config['Server']['LogFile'])
                else:
                    self.comboNodeAddress.setEnabled(False)
                    self.comboNodeLogFile.setEnabled(False)
            except Exception as e:
                print(self.LOG_TAG + "exception loading config: ", e)

    def _load_node_config(self, addr):
        try:
            if self.comboNodeAddress.currentText() == "":
                return None, QC.translate("preferences", "Server address can not be empty")

            node_action = Config.ACTION_DENY
            if self.comboNodeAction.currentIndex() == 1:
                node_action = Config.ACTION_ALLOW

            node_duration = Config.DURATION_ONCE
            if self.comboNodeDuration.currentIndex() == 1:
                node_duration = Config.DURATION_UNTIL_RESTART
            elif self.comboNodeDuration.currentIndex() == 2:
                node_duration = Config.DURATION_ALWAYS

            node_config = json.loads(self._nodes.get_node_config(addr))
            node_config['DefaultAction'] = node_action
            node_config['DefaultDuration'] = node_duration
            node_config['ProcMonitorMethod'] = self.comboNodeMonitorMethod.currentText()
            node_config['LogLevel'] = self.comboNodeLogLevel.currentIndex()
            node_config['InterceptUnknown'] = self.checkInterceptUnknown.isChecked()

            if node_config.get('Server') != None:
                # skip setting Server Address if we're applying the config to all nodes
                if self.checkApplyToNodes.isChecked():
                    node_config['Server']['Address'] = self.comboNodeAddress.currentText()
                node_config['Server']['LogFile'] = self.comboNodeLogFile.currentText()
            else:
                print(addr, " doesn't have Server item")
            return json.dumps(node_config, indent="    "), None
        except Exception as e:
            print(self.LOG_TAG + "exception loading node config on %s: " % addr, e)

        return None, QC.translate("preferences", "Error loading {0} configuration").format(addr)

    def _load_ui_columns_config(self):
        cols = self._cfg.getSettings(Config.STATS_SHOW_COLUMNS)
        if cols == None:
            return

        for c in range(7):
            checked = str(c) in cols
            if c == 0:
                self.checkHideTime.setChecked(checked)
            elif c == 1:
                self.checkHideNode.setChecked(checked)
            elif c == 2:
                self.checkHideAction.setChecked(checked)
            elif c == 3:
                self.checkHideDst.setChecked(checked)
            elif c == 4:
                self.checkHideProto.setChecked(checked)
            elif c == 5:
                self.checkHideProc.setChecked(checked)
            elif c == 6:
                self.checkHideRule.setChecked(checked)

    def _reset_node_settings(self):
        self.comboNodeAction.setCurrentIndex(0)
        self.comboNodeDuration.setCurrentIndex(0)
        self.comboNodeMonitorMethod.setCurrentIndex(0)
        self.checkInterceptUnknown.setChecked(False)
        self.comboNodeLogLevel.setCurrentIndex(0)
        self.labelNodeName.setText("")
        self.labelNodeVersion.setText("")

    def _save_settings(self):
        self._save_ui_config()
        self._save_db_config()

        if self.tabWidget.currentIndex() == self.TAB_NODES:
            self._show_status_label()

            addr = self.comboNodes.currentText()
            if (self._node_needs_update or self.checkApplyToNodes.isChecked()) and addr != "":
                try:
                    notif = ui_pb2.Notification(
                            id=int(str(time.time()).replace(".", "")),
                            type=ui_pb2.CHANGE_CONFIG,
                            data="",
                            rules=[])
                    if self.checkApplyToNodes.isChecked():
                        for addr in self._nodes.get_nodes():
                            error = self._save_node_config(notif, addr)
                            if error != None:
                                self._set_status_error(error)
                                return
                    else:
                        error = self._save_node_config(notif, addr)
                        if error != None:
                            self._set_status_error(error)
                            return
                except Exception as e:
                    print(self.LOG_TAG + "exception saving config: ", e)
                    self._set_status_error(QC.translate("preferences", "Exception saving config: {0}").format(str(e)))

            self._node_needs_update = False

        self.saved.emit()
        self._settingsSaved = True

    def _save_db_config(self):
        dbtype = self.comboDBType.currentIndex()
        self._cfg.setSettings(Config.DEFAULT_DB_TYPE_KEY, dbtype)
        self._cfg.setSettings(Config.DEFAULT_DB_PURGE_OLDEST, bool(self.checkDBMaxDays.isChecked()))
        self._cfg.setSettings(Config.DEFAULT_DB_MAX_DAYS, int(self.spinDBMaxDays.value()))
        self._cfg.setSettings(Config.DEFAULT_DB_PURGE_INTERVAL, int(self.spinDBPurgeInterval.value()))

        if self.comboDBType.currentIndex() == self.dbType:
            return

        if dbtype == self._db.get_db_file():
            return

        if self.comboDBType.currentIndex() != Database.DB_TYPE_MEMORY:
            if self.dbLabel.text() != "":
                self._cfg.setSettings(Config.DEFAULT_DB_FILE_KEY, self.dbLabel.text())
            else:
                Message.ok(
                    QC.translate("preferences", "Warning"),
                    QC.translate("preferences", "You must select a file for the database<br>or choose \"In memory\" type."),
                    QtWidgets.QMessageBox.Warning)
                return

        Message.ok(
            QC.translate("preferences", "DB type changed"),
            QC.translate("preferences", "Restart the GUI in order effects to take effect"),
            QtWidgets.QMessageBox.Warning)

        self.dbType = self.comboDBType.currentIndex()

    def _save_ui_config(self):
        self._save_ui_columns_config()

        self._cfg.setSettings(self._cfg.DEFAULT_IGNORE_TEMPORARY_RULES, int(self.comboUIRules.currentIndex()))
        self._cfg.setSettings(self._cfg.DEFAULT_IGNORE_RULES, bool(self.checkUIRules.isChecked()))
        #self._set_rules_duration_filter()
        self._cfg.setRulesDurationFilter(
            bool(self.checkUIRules.isChecked()),
            int(self.comboUIRules.currentIndex())
        )

        self._cfg.setSettings(self._cfg.DEFAULT_ACTION_KEY, self.comboUIAction.currentIndex())
        self._cfg.setSettings(self._cfg.DEFAULT_DURATION_KEY, int(self.comboUIDuration.currentIndex()))
        self._cfg.setSettings(self._cfg.DEFAULT_TARGET_KEY, self.comboUITarget.currentIndex())
        self._cfg.setSettings(self._cfg.DEFAULT_TIMEOUT_KEY, self.spinUITimeout.value())
        self._cfg.setSettings(self._cfg.DEFAULT_DISABLE_POPUPS, bool(self.popupsCheck.isChecked()))
        self._cfg.setSettings(self._cfg.DEFAULT_POPUP_POSITION, int(self.comboUIDialogPos.currentIndex()))

        self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED, bool(self.showAdvancedCheck.isChecked()))
        self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_DSTIP, bool(self.dstIPCheck.isChecked()))
        self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_DSTPORT, bool(self.dstPortCheck.isChecked()))
        self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_UID, bool(self.uidCheck.isChecked()))

        self._cfg.setSettings(self._cfg.NOTIFICATIONS_ENABLED, bool(self.groupNotifs.isChecked()))
        self._cfg.setSettings(self._cfg.NOTIFICATIONS_TYPE,
                              int(Config.NOTIFICATION_TYPE_SYSTEM if self.radioSysNotifs.isChecked() else Config.NOTIFICATION_TYPE_QT))

        # this is a workaround for not display pop-ups.
        # see #79 for more information.
        if self.popupsCheck.isChecked():
            self._cfg.setSettings(self._cfg.DEFAULT_TIMEOUT_KEY, 0)

    def _save_ui_columns_config(self):
        cols=list()
        if self.checkHideTime.isChecked():
            cols.append("0")
        if self.checkHideNode.isChecked():
            cols.append("1")
        if self.checkHideAction.isChecked():
            cols.append("2")
        if self.checkHideDst.isChecked():
            cols.append("3")
        if self.checkHideProto.isChecked():
            cols.append("4")
        if self.checkHideProc.isChecked():
            cols.append("5")
        if self.checkHideRule.isChecked():
            cols.append("6")

        self._cfg.setSettings(Config.STATS_SHOW_COLUMNS, cols)

    def _save_node_config(self, notifObject, addr):
        try:
            self._set_status_message(QC.translate("preferences", "Applying configuration on {0} ...").format(addr))
            notifObject.data, error = self._load_node_config(addr)
            if error != None:
                return error

            if addr.startswith("unix://"):
                self._cfg.setSettings(self._cfg.DEFAULT_DEFAULT_SERVER_ADDR, self.comboNodeAddress.currentText())
            else:
                self._nodes.save_node_config(addr, notifObject.data)
                nid = self._nodes.send_notification(addr, notifObject, self._notification_callback)

                self._notifications_sent[nid] = notifObject
        except Exception as e:
            print(self.LOG_TAG + "exception saving node config on %s: " % addr, e)
            self._set_status_error(QC.translate("Exception saving node config {0}: {1}").format((addr, str(e))))
            return addr + ": " + str(e)

        return None

    def _hide_status_label(self):
        self.statusLabel.hide()

    def _show_status_label(self):
        self.statusLabel.show()

    def _set_status_error(self, msg):
        self._show_status_label()
        self.statusLabel.setStyleSheet('color: red')
        self.statusLabel.setText(msg)

    def _set_status_successful(self, msg):
        self._show_status_label()
        self.statusLabel.setStyleSheet('color: green')
        self.statusLabel.setText(msg)

    def _set_status_message(self, msg):
        self._show_status_label()
        self.statusLabel.setStyleSheet('color: darkorange')
        self.statusLabel.setText(msg)

    def _reset_status_message(self):
        self.statusLabel.setText("")
        self._hide_status_label()

    def _enable_db_cleaner_options(self, enable, db_max_days):
        self.checkDBMaxDays.setChecked(enable)
        self.spinDBMaxDays.setEnabled(enable)
        self.spinDBPurgeInterval.setEnabled(enable)
        self.labelDBPurgeInterval.setEnabled(enable)
        self.cmdDBMaxDaysUp.setEnabled(enable)
        self.cmdDBMaxDaysDown.setEnabled(enable)
        self.cmdDBPurgesUp.setEnabled(enable)
        self.cmdDBPurgesDown.setEnabled(enable)

    @QtCore.pyqtSlot(ui_pb2.NotificationReply)
    def _cb_notification_callback(self, reply):
        #print(self.LOG_TAG, "Config notification received: ", reply.id, reply.code)
        if reply.id in self._notifications_sent:
            if reply.code == ui_pb2.OK:
                self._set_status_successful(QC.translate("preferences", "Configuration applied."))
            else:
                self._set_status_error(QC.translate("preferences", "Error applying configuration: {0}").format(reply.data))

            del self._notifications_sent[reply.id]

    def _cb_file_db_clicked(self):
        options = QtWidgets.QFileDialog.Options()
        fileName, _ = QtWidgets.QFileDialog.getSaveFileName(self, "", "","All Files (*)", options=options)
        if fileName:
            self.dbLabel.setText(fileName)

    def _cb_db_type_changed(self):
        if self.comboDBType.currentIndex() == Database.DB_TYPE_MEMORY:
            self.dbFileButton.setVisible(False)
            self.dbLabel.setVisible(False)
        else:
            self.dbFileButton.setVisible(True)
            self.dbLabel.setVisible(True)

    def _cb_accept_button_clicked(self):
        self.accept()
        if not self._settingsSaved:
            self._save_settings()

    def _cb_apply_button_clicked(self):
        self._save_settings()

    def _cb_cancel_button_clicked(self):
        self.reject()

    def _cb_help_button_clicked(self):
        QuickHelp.show(
            QC.translate("preferences",
                         "Hover the mouse over the texts to display the help<br><br>Don't forget to visit the wiki: <a href=\"{0}\">{0}</a>"
                         ).format(Config.HELP_URL)
        )

    def _cb_popups_check_toggled(self, checked):
        self.spinUITimeout.setEnabled(not checked)
        if not checked:
            self.spinUITimeout.setValue(15)

    def _cb_node_combo_changed(self, index):
        self._load_node_settings()

    def _cb_node_needs_update(self):
        self._node_needs_update = True

    def _cb_check_ui_rules_toggled(self, state):
        self.comboUIRules.setEnabled(state)

    def _cb_db_max_days_toggled(self, state):
        self._enable_db_cleaner_options(state, 1)

    def _cb_cmd_spin_clicked(self, spinWidget, operation):
        if operation == self.SUM:
            spinWidget.setValue(spinWidget.value() + 1)
        else:
            spinWidget.setValue(spinWidget.value() - 1)

    def _cb_radio_system_notifications(self):
        if self._desktop_notifications.is_available() == False:
            self.radioSysNotifs.setChecked(False)
            self.radioQtNotifs.setChecked(True)
            self._set_status_error(QC.translate("notifications", "System notifications are not available, you need to install python3-notify2."))
            return

    def _cb_test_notifs_clicked(self):
        if self._desktop_notifications.is_available() == False:
            self._set_status_error(QC.translate("notifications", "System notifications are not available, you need to install python3-notify2."))
            return

        if self.radioSysNotifs.isChecked():
            self._desktop_notifications.show("title", "body")
        else:
            pass
Ejemplo n.º 3
0
    def __init__(self, parent=None, appicon=None):
        QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)

        self._themes = Themes.instance()
        self._saved_theme = ""

        self._cfg = Config.get()
        self._nodes = Nodes.instance()
        self._db = Database.instance()

        self._notification_callback.connect(self._cb_notification_callback)
        self._notifications_sent = {}
        self._desktop_notifications = DesktopNotifications()

        self.setupUi(self)
        self.setWindowIcon(appicon)

        self.dbFileButton.setVisible(False)
        self.dbLabel.setVisible(False)
        self.dbType = None

        self.acceptButton.clicked.connect(self._cb_accept_button_clicked)
        self.applyButton.clicked.connect(self._cb_apply_button_clicked)
        self.cancelButton.clicked.connect(self._cb_cancel_button_clicked)
        self.helpButton.clicked.connect(self._cb_help_button_clicked)
        self.popupsCheck.clicked.connect(self._cb_popups_check_toggled)
        self.dbFileButton.clicked.connect(self._cb_file_db_clicked)
        self.checkUIRules.toggled.connect(self._cb_check_ui_rules_toggled)
        self.cmdTimeoutUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.SUM))
        self.cmdTimeoutDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinUITimeout, self.REST))
        self.cmdDBMaxDaysUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.SUM))
        self.cmdDBMaxDaysDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.REST))
        self.cmdDBPurgesUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.SUM))
        self.cmdDBPurgesDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.REST))
        self.cmdTestNotifs.clicked.connect(self._cb_test_notifs_clicked)
        self.radioSysNotifs.clicked.connect(self._cb_radio_system_notifications)
        self.helpButton.setToolTipDuration(30 * 1000)

        if QtGui.QIcon.hasThemeIcon("emblem-default"):
            return

        saveIcon = Icons.new("document-save")
        applyIcon = Icons.new("emblem-default")
        delIcon = Icons.new("edit-delete")
        closeIcon = Icons.new("window-close")
        openIcon = Icons.new("document-open")
        helpIcon = Icons.new("help-browser")
        addIcon = Icons.new("list-add")
        delIcon = Icons.new("list-remove")
        self.applyButton.setIcon(applyIcon)
        self.cancelButton.setIcon(closeIcon)
        self.acceptButton.setIcon(saveIcon)
        self.helpButton.setIcon(helpIcon)
        self.dbFileButton.setIcon(openIcon)

        self.cmdTimeoutUp.setIcon(addIcon)
        self.cmdTimeoutDown.setIcon(delIcon)
        self.cmdDBMaxDaysUp.setIcon(addIcon)
        self.cmdDBMaxDaysDown.setIcon(delIcon)
        self.cmdDBPurgesUp.setIcon(addIcon)
        self.cmdDBPurgesDown.setIcon(delIcon)
Ejemplo n.º 4
0
    def __init__(self, app, on_exit):
        super(UIService, self).__init__()

        self.MENU_ENTRY_STATS = QtCore.QCoreApplication.translate(
            "contextual_menu", "Statistics")
        self.MENU_ENTRY_FW_ENABLE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Enable")
        self.MENU_ENTRY_FW_DISABLE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Disable")
        self.MENU_ENTRY_HELP = QtCore.QCoreApplication.translate(
            "contextual_menu", "Help")
        self.MENU_ENTRY_CLOSE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Close")

        # set of actions that must be performed on the main thread
        self.NODE_ADD = 0
        self.NODE_UPDATE = 1
        self.NODE_DELETE = 2
        self.ADD_RULE = 3
        self.DELETE_RULE = 4

        self._cfg = Config.init()
        self._db = Database.instance()
        db_file = self._cfg.getSettings(self._cfg.DEFAULT_DB_FILE_KEY)
        db_status, db_error = self._db.initialize(dbtype=self._cfg.getInt(
            self._cfg.DEFAULT_DB_TYPE_KEY),
                                                  dbfile=db_file)
        if db_status is False:
            Message.ok(
                QtCore.QCoreApplication.translate("preferences", "Warning"),
                QtCore.QCoreApplication.translate(
                    "preferences",
                    "The DB is corrupted and it's not safe to continue.<br>\
                                                  Remove, backup or recover the file before continuing.<br><br>\
                                                  Corrupted database file: {0}"
                    .format(db_file)), QtWidgets.QMessageBox.Warning)
            sys.exit(-1)

        self._db_sqlite = self._db.get_db()
        self._last_ping = None
        self._version_warning_shown = False
        self._asking = False
        self._connected = False
        self._fw_enabled = False
        self._path = os.path.abspath(os.path.dirname(__file__))
        self._app = app
        self._on_exit = on_exit
        self._exit = False
        self._msg = QtWidgets.QMessageBox()
        self._remote_lock = Lock()
        self._remote_stats = {}

        self._desktop_notifications = DesktopNotifications()
        self._setup_interfaces()
        self._setup_icons()
        self._prompt_dialog = PromptDialog(appicon=self.white_icon)
        self._stats_dialog = StatsDialog(dbname="general",
                                         db=self._db,
                                         appicon=self.white_icon)
        self._setup_tray()
        self._setup_slots()

        self._nodes = Nodes.instance()

        self._last_stats = {}
        self._last_items = {
            'hosts': {},
            'procs': {},
            'addrs': {},
            'ports': {},
            'users': {}
        }

        self._show_gui_if_tray_not_available()

        self._cleaner = None
        if self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST):
            self._start_db_cleaner()
Ejemplo n.º 5
0
class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
    _new_remote_trigger = QtCore.pyqtSignal(str, ui_pb2.PingRequest)
    _node_actions_trigger = QtCore.pyqtSignal(dict)
    _update_stats_trigger = QtCore.pyqtSignal(str, str, ui_pb2.PingRequest)
    _version_warning_trigger = QtCore.pyqtSignal(str, str)
    _status_change_trigger = QtCore.pyqtSignal(bool)
    _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
    _show_message_trigger = QtCore.pyqtSignal(str, str, int)

    # .desktop filename located under /usr/share/applications/
    DESKTOP_FILENAME = "opensnitch_ui.desktop"

    def __init__(self, app, on_exit):
        super(UIService, self).__init__()

        self.MENU_ENTRY_STATS = QtCore.QCoreApplication.translate(
            "contextual_menu", "Statistics")
        self.MENU_ENTRY_FW_ENABLE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Enable")
        self.MENU_ENTRY_FW_DISABLE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Disable")
        self.MENU_ENTRY_HELP = QtCore.QCoreApplication.translate(
            "contextual_menu", "Help")
        self.MENU_ENTRY_CLOSE = QtCore.QCoreApplication.translate(
            "contextual_menu", "Close")

        # set of actions that must be performed on the main thread
        self.NODE_ADD = 0
        self.NODE_UPDATE = 1
        self.NODE_DELETE = 2
        self.ADD_RULE = 3
        self.DELETE_RULE = 4

        self._cfg = Config.init()
        self._db = Database.instance()
        db_file = self._cfg.getSettings(self._cfg.DEFAULT_DB_FILE_KEY)
        db_status, db_error = self._db.initialize(dbtype=self._cfg.getInt(
            self._cfg.DEFAULT_DB_TYPE_KEY),
                                                  dbfile=db_file)
        if db_status is False:
            Message.ok(
                QtCore.QCoreApplication.translate("preferences", "Warning"),
                QtCore.QCoreApplication.translate(
                    "preferences",
                    "The DB is corrupted and it's not safe to continue.<br>\
                                                  Remove, backup or recover the file before continuing.<br><br>\
                                                  Corrupted database file: {0}"
                    .format(db_file)), QtWidgets.QMessageBox.Warning)
            sys.exit(-1)

        self._db_sqlite = self._db.get_db()
        self._last_ping = None
        self._version_warning_shown = False
        self._asking = False
        self._connected = False
        self._fw_enabled = False
        self._path = os.path.abspath(os.path.dirname(__file__))
        self._app = app
        self._on_exit = on_exit
        self._exit = False
        self._msg = QtWidgets.QMessageBox()
        self._remote_lock = Lock()
        self._remote_stats = {}

        self._desktop_notifications = DesktopNotifications()
        self._setup_interfaces()
        self._setup_icons()
        self._prompt_dialog = PromptDialog(appicon=self.white_icon)
        self._stats_dialog = StatsDialog(dbname="general",
                                         db=self._db,
                                         appicon=self.white_icon)
        self._setup_tray()
        self._setup_slots()

        self._nodes = Nodes.instance()

        self._last_stats = {}
        self._last_items = {
            'hosts': {},
            'procs': {},
            'addrs': {},
            'ports': {},
            'users': {}
        }

        self._show_gui_if_tray_not_available()

        self._cleaner = None
        if self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST):
            self._start_db_cleaner()

    # https://gist.github.com/pklaus/289646
    def _setup_interfaces(self):
        namestr, outbytes = Utils.get_interfaces()
        self._interfaces = {}
        for i in range(0, outbytes, 40):
            name = namestr[i:i + 16].split(b'\0', 1)[0]
            addr = namestr[i + 20:i + 24]
            self._interfaces[name] = "%d.%d.%d.%d" % (int(addr[0]), int(
                addr[1]), int(addr[2]), int(addr[3]))

    def _setup_slots(self):
        # https://stackoverflow.com/questions/40288921/pyqt-after-messagebox-application-quits-why
        self._app.setQuitOnLastWindowClosed(False)
        self._version_warning_trigger.connect(self._on_diff_versions)
        self._new_remote_trigger.connect(self._on_new_remote)
        self._node_actions_trigger.connect(self._on_node_actions)
        self._update_stats_trigger.connect(self._on_update_stats)
        self._status_change_trigger.connect(self._on_status_changed)
        self._stats_dialog._shown_trigger.connect(self._on_stats_dialog_shown)
        self._stats_dialog._status_changed_trigger.connect(
            self._on_stats_status_changed)
        self._stats_dialog.settings_saved.connect(self._on_settings_saved)
        self._show_message_trigger.connect(self._show_systray_message)

    def _setup_icons(self):
        self.off_image = QtGui.QPixmap(
            os.path.join(self._path, "res/icon-off.png"))
        self.off_icon = QtGui.QIcon()
        self.off_icon.addPixmap(self.off_image, QtGui.QIcon.Normal,
                                QtGui.QIcon.Off)
        self.white_image = QtGui.QPixmap(
            os.path.join(self._path, "res/icon-white.svg"))
        self.white_icon = QtGui.QIcon()
        self.white_icon.addPixmap(self.white_image, QtGui.QIcon.Normal,
                                  QtGui.QIcon.Off)
        self.red_image = QtGui.QPixmap(
            os.path.join(self._path, "res/icon-red.png"))
        self.red_icon = QtGui.QIcon()
        self.red_icon.addPixmap(self.red_image, QtGui.QIcon.Normal,
                                QtGui.QIcon.Off)
        self.pause_image = QtGui.QPixmap(
            os.path.join(self._path, "res/icon-pause.png"))
        self.pause_icon = QtGui.QIcon()
        self.pause_icon.addPixmap(self.pause_image, QtGui.QIcon.Normal,
                                  QtGui.QIcon.Off)
        self.alert_image = QtGui.QPixmap(
            os.path.join(self._path, "res/icon-alert.png"))
        self.alert_icon = QtGui.QIcon()
        self.alert_icon.addPixmap(self.alert_image, QtGui.QIcon.Normal,
                                  QtGui.QIcon.Off)

        self._app.setWindowIcon(self.white_icon)
        # NOTE: only available since pyqt 5.7
        if hasattr(self._app, "setDesktopFileName"):
            self._app.setDesktopFileName(self.DESKTOP_FILENAME)

    def _setup_tray(self):
        self._tray = QtWidgets.QSystemTrayIcon(self.off_icon)
        self._tray.show()

        self._menu = QtWidgets.QMenu()
        self._tray.setContextMenu(self._menu)
        self._tray.activated.connect(self._on_tray_icon_activated)

        self._menu.addAction(self.MENU_ENTRY_STATS).triggered.connect(
            self._show_stats_dialog)
        self._menu_enable_fw = self._menu.addAction(self.MENU_ENTRY_FW_DISABLE)
        self._menu_enable_fw.setEnabled(False)
        self._menu_enable_fw.triggered.connect(
            self._on_enable_interception_clicked)
        self._menu.addAction(self.MENU_ENTRY_HELP).triggered.connect(
            lambda: QtGui.QDesktopServices.openUrl(
                QtCore.QUrl(Config.HELP_CONFIG_URL)))
        self._menu.addAction(self.MENU_ENTRY_CLOSE).triggered.connect(
            self._on_close)

    def _show_gui_if_tray_not_available(self):
        """If the system tray is not available or ready, show the GUI after
        10s. This delay helps to skip showing up the GUI when DEs' autologin is on.
        """
        tray = self._tray
        gui = self._stats_dialog

        def __show_gui():
            if not tray.isSystemTrayAvailable():
                gui.show()

        QtCore.QTimer.singleShot(10000, __show_gui)

    def _on_tray_icon_activated(self, reason):
        if reason == QtWidgets.QSystemTrayIcon.Trigger or reason == QtWidgets.QSystemTrayIcon.MiddleClick:
            if self._stats_dialog.isVisible(
            ) and not self._stats_dialog.isMinimized():
                self._stats_dialog.hide()
            elif self._stats_dialog.isVisible(
            ) and self._stats_dialog.isMinimized(
            ) and not self._stats_dialog.isMaximized():
                self._stats_dialog.hide()
                self._stats_dialog.showNormal()
            elif self._stats_dialog.isVisible(
            ) and self._stats_dialog.isMinimized(
            ) and self._stats_dialog.isMaximized():
                self._stats_dialog.hide()
                self._stats_dialog.showMaximized()
            else:
                self._stats_dialog.show()

    def _on_close(self):
        self._exit = True
        self._tray.setIcon(self.off_icon)
        self._app.processEvents()
        self._nodes.stop_notifications()
        self._nodes.update_all(Nodes.OFFLINE)
        self._db.vacuum()
        self._db.optimize()
        self._db.close()
        self._stop_db_cleaner()
        self._on_exit()

    def _show_stats_dialog(self):
        if self._connected and self._fw_enabled:
            self._tray.setIcon(self.white_icon)
        self._stats_dialog.show()

    @QtCore.pyqtSlot(bool)
    def _on_stats_status_changed(self, enabled):
        self._update_fw_status(enabled)

    @QtCore.pyqtSlot(bool)
    def _on_status_changed(self, enabled):
        self._set_daemon_connected(enabled)

    @QtCore.pyqtSlot(str, str)
    def _on_diff_versions(self, daemon_ver, ui_ver):
        if self._version_warning_shown == False:
            self._msg.setIcon(QtWidgets.QMessageBox.Warning)
            self._msg.setWindowTitle("OpenSnitch version mismatch!")
            self._msg.setText(("You are running version <b>%s</b> of the daemon, while the UI is at version " + \
                              "<b>%s</b>, they might not be fully compatible.") % (daemon_ver, ui_ver))
            self._msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
            self._msg.show()
            self._version_warning_shown = True

    @QtCore.pyqtSlot(str, str, ui_pb2.PingRequest)
    def _on_update_stats(self, proto, addr, request):
        main_need_refresh, details_need_refresh = self._populate_stats(
            self._db, proto, addr, request.stats)
        is_local_request = self._is_local_request(proto, addr)
        self._stats_dialog.update(is_local_request, request.stats,
                                  main_need_refresh or details_need_refresh)

    @QtCore.pyqtSlot(str, ui_pb2.PingRequest)
    def _on_new_remote(self, addr, request):
        self._remote_stats[addr] = {
            'last_ping': datetime.now(),
            'dialog': StatsDialog(address=addr, dbname=addr, db=self._db)
        }
        self._remote_stats[addr]['dialog'].daemon_connected = True
        self._remote_stats[addr]['dialog'].update(addr, request.stats)
        self._remote_stats[addr]['dialog'].show()

    @QtCore.pyqtSlot()
    def _on_stats_dialog_shown(self):
        if self._connected:
            if self._fw_enabled:
                self._tray.setIcon(self.white_icon)
            else:
                self._tray.setIcon(self.pause_icon)
        else:
            self._tray.setIcon(self.off_icon)

    @QtCore.pyqtSlot(ui_pb2.NotificationReply)
    def _on_notification_reply(self, reply):
        if reply.code == ui_pb2.ERROR:
            self._tray.showMessage("Error", reply.data,
                                   QtWidgets.QSystemTrayIcon.Information, 5000)

    def _on_remote_stats_menu(self, address):
        self._remote_stats[address]['dialog'].show()

    @QtCore.pyqtSlot(str, str, int)
    def _show_systray_message(self, title, body, icon):
        if self._desktop_notifications.are_enabled():
            timeout = self._cfg.getInt(Config.DEFAULT_TIMEOUT_KEY, 15)

            if self._desktop_notifications.is_available() and self._cfg.getInt(
                    Config.NOTIFICATIONS_TYPE,
                    1) == Config.NOTIFICATION_TYPE_SYSTEM:
                self._desktop_notifications.show(
                    title, body, os.path.join(self._path,
                                              "res/icon-white.svg"))
            else:
                self._tray.showMessage(title, body, icon, timeout * 1000)

        if icon == QtWidgets.QSystemTrayIcon.NoIcon:
            self._tray.setIcon(self.alert_icon)

    def _on_enable_interception_clicked(self):
        self._enable_interception(self._fw_enabled)

    @QtCore.pyqtSlot()
    def _on_settings_saved(self):
        if self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST):
            if self._cleaner != None:
                self._stop_db_cleaner()
            self._start_db_cleaner()
        elif self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST
                               ) == False and self._cleaner != None:
            self._stop_db_cleaner()

    def _stop_db_cleaner(self):
        if self._cleaner != None:
            self._cleaner.stop()
            self._cleaner = None

    def _start_db_cleaner(self):
        def _cleaner_task(db):
            oldest = self._cfg.getInt(self._cfg.DEFAULT_DB_MAX_DAYS, 1)
            db.purge_oldest(oldest)

        interval = self._cfg.getInt(self._cfg.DEFAULT_DB_PURGE_INTERVAL, 5)
        self._cleaner = CleanerTask(interval, _cleaner_task)
        self._cleaner.start()

    def _update_fw_status(self, enabled):
        """_update_fw_status updates the status of the menu entry
        to disable or enable the firewall of the daemon.
        """
        self._fw_enabled = enabled
        if self._connected == False:
            return

        self._stats_dialog.update_interception_status(enabled)
        if enabled:
            self._tray.setIcon(self.white_icon)
            self._menu_enable_fw.setText(self.MENU_ENTRY_FW_DISABLE)
        else:
            self._tray.setIcon(self.pause_icon)
            self._menu_enable_fw.setText(self.MENU_ENTRY_FW_ENABLE)

    def _set_daemon_connected(self, connected):
        """_set_daemon_connected only updates the connection status of the daemon(s),
        regardless if the fw is enabled or not.
        There're 3 states:
            - daemon connected
            - daemon not connected
            - daemon connected and firewall enabled/disabled
        """
        self._stats_dialog.daemon_connected = connected
        self._connected = connected

        # if there're more than 1 node, override connection status
        if self._nodes.count() >= 1:
            self._connected = True
            self._stats_dialog.daemon_connected = True

        if self._nodes.count() == 1:
            self._menu_enable_fw.setEnabled(True)

        if self._nodes.count() == 0 or self._nodes.count() > 1:
            self._menu_enable_fw.setEnabled(False)

        self._stats_dialog.update_status()

        if self._connected:
            self._tray.setIcon(self.white_icon)
        else:
            self._fw_enabled = False
            self._tray.setIcon(self.off_icon)

    def _enable_interception(self, enable):
        if self._connected == False:
            return
        if self._nodes.count() == 0:
            self._tray.showMessage("No nodes connected", "",
                                   QtWidgets.QSystemTrayIcon.Information, 5000)
            return
        if self._nodes.count() > 1:
            print("enable interception for all nodes not supported yet")
            return

        if enable:
            nid, noti = self._nodes.stop_interception(
                _callback=self._notification_callback)
        else:
            nid, noti = self._nodes.start_interception(
                _callback=self._notification_callback)

        self._fw_enabled = not enable

        self._stats_dialog._status_changed_trigger.emit(not enable)

    def _is_local_request(self, proto, addr):
        if proto == "unix":
            return True

        elif proto == "ipv4" or proto == "ipv6":
            for name, ip in self._interfaces.items():
                if addr == ip:
                    return True

        return False

    def _get_peer(self, peer):
        """
        server          -> client
        127.0.0.1:50051 -> ipv4:127.0.0.1:52032
        [::]:50051      -> ipv6:[::1]:59680
        0.0.0.0:50051   -> ipv6:[::1]:59654
        """
        return self._nodes.get_addr(peer)

    def _delete_node(self, peer):
        try:
            proto, addr = self._get_peer(peer)
            if addr in self._last_stats:
                del self._last_stats[addr]
            for table in self._last_items:
                if addr in self._last_items[table]:
                    del self._last_items[table][addr]

            self._nodes.update(proto, addr, Nodes.OFFLINE)
            self._nodes.delete(peer)
            self._stats_dialog.update(True, None, True)
        except Exception as e:
            print("_delete_node() exception:", e)

    def _populate_stats(self, db, proto, addr, stats):
        main_need_refresh = False
        details_need_refresh = False
        try:
            if db == None:
                print("populate_stats() db None")
                return main_need_refresh, details_need_refresh

            _node = self._nodes.get_node(proto + ":" + addr)
            if _node == None:
                return main_need_refresh, details_need_refresh

            # TODO: move to nodes.add_node()
            version = _node['data'].version if _node != None else ""
            hostname = _node['data'].name if _node != None else ""
            db.insert("nodes",
                    "(addr, status, hostname, daemon_version, daemon_uptime, " \
                            "daemon_rules, cons, cons_dropped, version, last_connection)",
                            (addr, Nodes.ONLINE, hostname, stats.daemon_version, str(timedelta(seconds=stats.uptime)),
                            stats.rules, stats.connections, stats.dropped,
                            version, datetime.now().strftime("%Y-%m-%d %H:%M:%S")))

            if addr not in self._last_stats:
                self._last_stats[addr] = []

            db.transaction()
            for event in stats.events:
                if event.unixnano in self._last_stats[addr]:
                    continue
                main_need_refresh = True
                db.insert(
                    "connections",
                    "(time, node, action, protocol, src_ip, src_port, dst_ip, dst_host, dst_port, uid, pid, process, process_args, process_cwd, rule)",
                    (str(datetime.fromtimestamp(event.unixnano / 1000000000)),
                     "%s:%s" % (proto, addr), event.rule.action,
                     event.connection.protocol, event.connection.src_ip,
                     str(event.connection.src_port), event.connection.dst_ip,
                     event.connection.dst_host, str(event.connection.dst_port),
                     str(event.connection.user_id),
                     str(event.connection.process_id),
                     event.connection.process_path, " ".join(
                         event.connection.process_args),
                     event.connection.process_cwd, event.rule.name),
                    action_on_conflict="IGNORE")
                self._nodes.update_rule_time(
                    str(datetime.fromtimestamp(event.unixnano / 1000000000)),
                    event.rule.name, "%s:%s" % (proto, addr))
            db.commit()

            details_need_refresh = self._populate_stats_details(
                db, addr, stats)
            self._last_stats[addr] = []
            for event in stats.events:
                self._last_stats[addr].append(event.unixnano)
        except Exception as e:
            print("_populate_stats() exception: ", e)

        return main_need_refresh, details_need_refresh

    def _populate_stats_details(self, db, addr, stats):
        need_refresh = False
        changed = self._populate_stats_events(db, addr, stats, "hosts",
                                              ("what", "hits"), (1, 2),
                                              stats.by_host.items())
        if changed: need_refresh = True
        changed = self._populate_stats_events(db, addr, stats, "procs",
                                              ("what", "hits"), (1, 2),
                                              stats.by_executable.items())
        if changed: need_refresh = True
        changed = self._populate_stats_events(db, addr, stats, "addrs",
                                              ("what", "hits"), (1, 2),
                                              stats.by_address.items())
        if changed: need_refresh = True
        changed = self._populate_stats_events(db, addr, stats, "ports",
                                              ("what", "hits"), (1, 2),
                                              stats.by_port.items())
        if changed: need_refresh = True
        changed = self._populate_stats_events(db, addr, stats, "users",
                                              ("what", "hits"), (1, 2),
                                              stats.by_uid.items())
        if changed: need_refresh = True

        return need_refresh

    def _populate_stats_events(self, db, addr, stats, table, colnames, cols,
                               items):
        fields = []
        values = []
        need_refresh = False
        try:
            if addr not in self._last_items[table].keys():
                self._last_items[table][addr] = {}
            if items == self._last_items[table][addr]:
                return need_refresh

            for row, event in enumerate(items):
                if event in self._last_items[table][addr]:
                    continue
                need_refresh = True
                what, hits = event
                # FIXME: this is suboptimal
                # BUG: there can be users with same id on different machines but with different names
                if table == "users":
                    what = Utils.get_user_id(what)
                fields.append(what)
                values.append(int(hits))
            # FIXME: default action on conflict is to replace. If there're multiple nodes connected,
            # stats are painted once per node on each update.
            if need_refresh:
                db.insert_batch(table, colnames, cols, fields, values)

            self._last_items[table][addr] = items
        except Exception as e:
            print("details exception: ", e)

        return need_refresh

    def _overwrite_nodes_config(self, node_config):
        _default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY)
        temp_cfg = json.loads(node_config)
        try:
            if _default_action == Config.ACTION_DENY_IDX:
                temp_cfg['DefaultAction'] = Config.ACTION_DENY
            else:
                temp_cfg['DefaultAction'] = Config.ACTION_ALLOW

            node_config = json.dumps(temp_cfg)
        except Exception as e:
            print("error parsing node's configuration:", e)

        return node_config

    @QtCore.pyqtSlot(dict)
    def _on_node_actions(self, kwargs):
        if kwargs['action'] == self.NODE_ADD:
            n, addr = self._nodes.add(kwargs['peer'], kwargs['node_config'])
            if n != None:
                self._nodes.add_fw_rules(
                    addr,
                    FwRules.to_dict(
                        kwargs['node_config'].systemFirewall.SystemRules))
                self._status_change_trigger.emit(True)
                # if there're more than one node, we can't update the status
                # based on the fw status, only if the daemon is running or not
                if self._nodes.count() <= 1:
                    self._update_fw_status(
                        kwargs['node_config'].isFirewallRunning)
                else:
                    self._update_fw_status(True)
        elif kwargs['action'] == self.ADD_RULE:
            rule = kwargs['rule']
            proto, addr = self._get_peer(kwargs['peer'])
            self._nodes.add_rule(
                (datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
                "{0}:{1}".format(proto, addr), rule.name, rule.description,
                str(rule.enabled), str(rule.precedence), str(rule.nolog),
                rule.action, rule.duration, rule.operator.type,
                str(rule.operator.sensitive), rule.operator.operand,
                rule.operator.data)
        elif kwargs['action'] == self.DELETE_RULE:
            self._db.delete_rule(kwargs['name'], kwargs['addr'])

        elif kwargs['action'] == self.NODE_DELETE:
            self._delete_node(kwargs['peer'])

    def Ping(self, request, context):
        try:
            self._last_ping = datetime.now()
            if Utils.check_versions(request.stats.daemon_version):
                self._version_warning_trigger.emit(
                    request.stats.daemon_version, version)

            proto, addr = self._get_peer(context.peer())
            # do not update db here, do it on the main thread
            self._update_stats_trigger.emit(proto, addr, request)
            #else:
            #    with self._remote_lock:
            #        # XXX: disable this option for now
            #        # opening several dialogs only updates one of them.
            #        if addr not in self._remote_stats:
            #            self._new_remote_trigger.emit(addr, request)
            #        else:
            #            self._populate_stats(self._remote_stats[addr]['dialog'].get_db(), proto, addr, request.stats)
            #            self._remote_stats[addr]['dialog'].update(addr, request.stats)

        except Exception as e:
            print("Ping exception: ", e)

        return ui_pb2.PingReply(id=request.id)

    def AskRule(self, request, context):
        #def callback(ntf, action, connection):
        # TODO

        #if self._desktop_notifications.support_actions():
        #    self._desktop_notifications.ask(request, callback)

        # TODO: allow connections originated from ourselves: os.getpid() == request.pid)
        self._asking = True
        proto, addr = self._get_peer(context.peer())
        rule, timeout_triggered = self._prompt_dialog.promptUser(
            request, self._is_local_request(proto, addr), context.peer())
        self._last_ping = datetime.now()
        self._asking = False
        if rule == None:
            return None

        if timeout_triggered:
            _title = request.process_path
            if _title == "":
                _title = "%s:%d (%s)" % (request.dst_host if request.dst_host
                                         != "" else request.dst_ip,
                                         request.dst_port, request.protocol)

            node_text = "" if self._is_local_request(
                proto, addr) else "on node {0}:{1}".format(proto, addr)
            self._show_message_trigger.emit(
                _title, "{0} action applied {1}\nCommand line: {2}".format(
                    rule.action, node_text, " ".join(request.process_args)),
                QtWidgets.QSystemTrayIcon.NoIcon)

        if rule.duration in Config.RULES_DURATION_FILTER:
            self._node_actions_trigger.emit({
                'action': self.DELETE_RULE,
                'name': rule.name,
                'addr': context.peer()
            })
        else:
            self._node_actions_trigger.emit({
                'action': self.ADD_RULE,
                'peer': context.peer(),
                'rule': rule
            })

        return rule

    def Subscribe(self, node_config, context):
        """
        Accept and collect nodes. It keeps a connection open with each
        client, in order to send them notifications.

        @doc: https://grpc.github.io/grpc/python/grpc.html#service-side-context
        """
        # if the exit mark is set, don't accept new connections.
        # db vacuum operation may take a lot of time to complete.
        if self._exit:
            return
        try:
            self._node_actions_trigger.emit({
                'action': self.NODE_ADD,
                'peer': context.peer(),
                'node_config': node_config
            })
            # force events processing, to add the node ^ before the
            # Notifications() call arrives.
            self._app.processEvents()

            proto, addr = self._get_peer(context.peer())
            if self._is_local_request(proto, addr) == False:
                self._show_message_trigger.emit(
                    QtCore.QCoreApplication.translate("stats",
                                                      "New node connected"),
                    "({0})".format(context.peer()),
                    QtWidgets.QSystemTrayIcon.Information)
        except Exception as e:
            print("[Notifications] exception adding new node:", e)
            context.cancel()

        node_config.config = self._overwrite_nodes_config(node_config.config)

        return node_config

    def Notifications(self, node_iter, context):
        """
        Accept and collect nodes. It keeps a connection open with each
        client, in order to send them notifications.

        @doc: https://grpc.github.io/grpc/python/grpc.html#service-side-context
        @doc: https://grpc.io/docs/what-is-grpc/core-concepts/
        """
        proto, addr = self._get_peer(context.peer())
        _node = self._nodes.get_node("%s:%s" % (proto, addr))
        if _node == None:
            return

        stop_event = Event()

        def _on_client_closed():
            stop_event.set()
            self._node_actions_trigger.emit({
                'action': self.NODE_DELETE,
                'peer': context.peer(),
            })

            self._status_change_trigger.emit(False)
            # TODO: handle the situation when a node disconnects, and the
            # remaining node has the fw disabled.
            #if self._nodes.count() == 1:
            #    nd = self._nodes.get_nodes()
            #    if nd[0].get_config().isFirewallRunning:

            if self._is_local_request(proto, addr) == False:
                self._show_message_trigger.emit(
                    "node exited", "({0})".format(context.peer()),
                    QtWidgets.QSystemTrayIcon.Information)

        context.add_callback(_on_client_closed)

        # TODO: move to notifications.py
        def new_node_message():
            print("new node connected, listening for client responses...",
                  addr)

            while self._exit == False:
                try:
                    if stop_event.is_set():
                        break
                    in_message = next(node_iter)
                    if in_message == None:
                        continue

                    self._nodes.reply_notification(addr, in_message)
                except StopIteration:
                    print("[Notifications] Node {0} exited".format(addr))
                    break
                except grpc.RpcError as e:
                    print(
                        "[Notifications] grpc exception new_node_message(): ",
                        addr, in_message)
                except Exception as e:
                    print(
                        "[Notifications] unexpected exception new_node_message(): ",
                        addr, e, in_message)

        read_thread = Thread(target=new_node_message)
        read_thread.daemon = True
        read_thread.start()

        while self._exit == False:
            if stop_event.is_set():
                break

            try:
                noti = _node['notifications'].get()
                if noti != None:
                    _node['notifications'].task_done()
                    yield noti
            except Exception as e:
                print(
                    "[Notifications] exception getting notification from queue:",
                    addr, e)
                context.cancel()

        return node_iter