class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _prompt_trigger = QtCore.pyqtSignal() _tick_trigger = QtCore.pyqtSignal() _timeout_trigger = QtCore.pyqtSignal() def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) self.setupUi(self) self.setWindowTitle("OpenSnitch v%s" % version) self._cfg = Config.get() self._lock = threading.Lock() self._con = None self._rule = None self._local = True self._peer = None self._prompt_trigger.connect(self.on_connection_prompt_triggered) self._timeout_trigger.connect(self.on_timeout_triggered) self._tick_trigger.connect(self.on_tick_triggered) self._tick = self._cfg.default_timeout self._tick_thread = None self._done = threading.Event() self._apps_parser = LinuxDesktopParser() self._app_name_label = self.findChild(QtWidgets.QLabel, "appNameLabel") self._app_icon_label = self.findChild(QtWidgets.QLabel, "iconLabel") self._message_label = self.findChild(QtWidgets.QLabel, "messageLabel") self._src_ip_label = self.findChild(QtWidgets.QLabel, "sourceIPLabel") self._dst_ip_label = self.findChild(QtWidgets.QLabel, "destIPLabel") self._uid_label = self.findChild(QtWidgets.QLabel, "uidLabel") self._pid_label = self.findChild(QtWidgets.QLabel, "pidLabel") self._args_label = self.findChild(QtWidgets.QLabel, "argsLabel") self._apply_button = self.findChild(QtWidgets.QPushButton, "applyButton") self._apply_button.clicked.connect(self._on_apply_clicked) self._action_combo = self.findChild(QtWidgets.QComboBox, "actionCombo") self._what_combo = self.findChild(QtWidgets.QComboBox, "whatCombo") self._duration_combo = self.findChild(QtWidgets.QComboBox, "durationCombo") def promptUser(self, connection, is_local, peer): # one at a time with self._lock: # reset state self._tick = self._cfg.default_timeout self._tick_thread = threading.Thread(target=self._timeout_worker) self._rule = None self._local = is_local self._peer = peer self._con = connection self._done.clear() # trigger and show dialog self._prompt_trigger.emit() # start timeout thread self._tick_thread.start() # wait for user choice or timeout self._done.wait() return self._rule def _timeout_worker(self): while self._tick > 0 and self._done.is_set() is False: self._tick -= 1 self._tick_trigger.emit() time.sleep(1) if not self._done.is_set(): self._timeout_trigger.emit() @QtCore.pyqtSlot() def on_connection_prompt_triggered(self): self._render_connection(self._con) self.show() @QtCore.pyqtSlot() def on_tick_triggered(self): self._apply_button.setText("Apply (%d)" % self._tick) @QtCore.pyqtSlot() def on_timeout_triggered(self): self._on_apply_clicked() def _render_connection(self, con): if self._local: app_name, app_icon, _ = self._apps_parser.get_info_by_path( con.process_path, "terminal") else: app_name, app_icon = "", "terminal" if app_name == "": self._app_name_label.setText(con.process_path) else: self._app_name_label.setText(app_name) icon = QtGui.QIcon().fromTheme(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) self._app_icon_label.setPixmap(pixmap) if self._local: message = "<b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, con.dst_host or con.dst_ip, con.protocol, con.dst_port ) else: message = "The process <b>%s</b> running on the computer <b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, self._peer.split(':')[1], con.dst_host or con.dst_ip, con.protocol, con.dst_port ) self._message_label.setText(message) self._src_ip_label.setText(con.src_ip) self._dst_ip_label.setText(con.dst_ip) if self._local: uid = "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name) else: uid = "%d" % con.user_id self._uid_label.setText(uid) self._pid_label.setText("%s" % con.process_id) self._args_label.setText(' '.join(con.process_args)) self._what_combo.clear() self._what_combo.addItem("from this process") self._what_combo.addItem("from user %d" % con.user_id) self._what_combo.addItem("to port %d" % con.dst_port) self._what_combo.addItem("to %s" % con.dst_ip) if con.dst_host != "": self._what_combo.addItem("to %s" % con.dst_host) parts = con.dst_host.split('.')[1:] nparts = len(parts) for i in range(0, nparts - 1): self._what_combo.addItem("to *.%s" % '.'.join(parts[i:])) if self._cfg.default_action == "allow": self._action_combo.setCurrentIndex(0) else: self._action_combo.setCurrentIndex(1) if self._cfg.default_duration == "once": self._duration_combo.setCurrentIndex(0) elif self._cfg.default_duration == "until restart": self._duration_combo.setCurrentIndex(1) else: self._duration_combo.setCurrentIndex(2) self._what_combo.setCurrentIndex(0) self._apply_button.setText("Apply (%d)" % self._tick) self.setFixedSize(self.size()) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): if not event.key() == QtCore.Qt.Key_Escape: super(PromptDialog, self).keyPressEvent(event) # prevent a click on the window's x # from quitting the whole application def closeEvent(self, e): self._on_apply_clicked() e.ignore() def _on_apply_clicked(self): self._rule = ui_pb2.Rule(name="user.choice") action_idx = self._action_combo.currentIndex() if action_idx == 0: self._rule.action = "allow" else: self._rule.action = "deny" duration_idx = self._duration_combo.currentIndex() if duration_idx == 0: self._rule.duration = "once" elif duration_idx == 1: self._rule.duration = "until restart" else: self._rule.duration = "always" what_idx = self._what_combo.currentIndex() if what_idx == 0: self._rule.operator.type = "simple" self._rule.operator.operand = "process.path" self._rule.operator.data = self._con.process_path elif what_idx == 1: self._rule.operator.type = "simple" self._rule.operator.operand = "user.id" self._rule.operator.data = "%s" % self._con.user_id elif what_idx == 2: self._rule.operator.type = "simple" self._rule.operator.operand = "dest.port" self._rule.operator.data = "%s" % self._con.dst_port elif what_idx == 3: self._rule.operator.type = "simple" self._rule.operator.operand = "dest.ip" self._rule.operator.data = self._con.dst_ip elif what_idx == 4: self._rule.operator.type = "simple" self._rule.operator.operand = "dest.host" self._rule.operator.data = self._con.dst_host else: self._rule.operator.type = "regexp" self._rule.operator.operand = "dest.host" self._rule.operator.data = ".*\.%s" % '\.'.join( self._con.dst_host.split('.')[what_idx - 4:]) self._rule.name = slugify("%s %s %s" % (self._rule.action, self._rule.operator.type, self._rule.operator.data)) self.hide() # signal that the user took a decision and # a new rule is available self._done.set()
class ProcessDetailsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): LOG_TAG = "[ProcessDetails]: " _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply) TAB_STATUS = 0 TAB_DESCRIPTORS = 1 TAB_IOSTATS = 2 TAB_MAPS = 3 TAB_STACK = 4 TAB_ENVS = 5 TABS = { TAB_STATUS: { "text": None, "scrollPos": 0 }, TAB_DESCRIPTORS: { "text": None, "scrollPos": 0 }, TAB_IOSTATS: { "text": None, "scrollPos": 0 }, TAB_MAPS: { "text": None, "scrollPos": 0 }, TAB_STACK: { "text": None, "scrollPos": 0 }, TAB_ENVS: { "text": None, "scrollPos": 0 } } def __init__(self, parent=None): super(ProcessDetailsDialog, self).__init__(parent) QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) self.setWindowFlags(QtCore.Qt.Window) self.setupUi(self) self._app_name = None self._app_icon = None self._apps_parser = LinuxDesktopParser() self._nodes = Nodes.instance() self._notification_callback.connect(self._cb_notification_callback) self._nid = None self._pid = "" self._notifications_sent = {} self.cmdClose.clicked.connect(self._cb_close_clicked) self.cmdAction.clicked.connect(self._cb_action_clicked) self.comboPids.currentIndexChanged.connect(self._cb_combo_pids_changed) self.TABS[self.TAB_STATUS]['text'] = self.textStatus self.TABS[self.TAB_DESCRIPTORS]['text'] = self.textOpenedFiles self.TABS[self.TAB_IOSTATS]['text'] = self.textIOStats self.TABS[self.TAB_MAPS]['text'] = self.textMappedFiles self.TABS[self.TAB_STACK]['text'] = self.textStack self.TABS[self.TAB_ENVS]['text'] = self.textEnv self.TABS[self.TAB_DESCRIPTORS]['text'].setFont(QtGui.QFont("monospace")) self.iconStart = QtGui.QIcon.fromTheme("media-playback-start") self.iconPause = QtGui.QIcon.fromTheme("media-playback-pause") if QtGui.QIcon.hasThemeIcon("window-close") == False: self.cmdClose.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton"))) self.iconStart = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPlay")) self.iconPause = self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_MediaPause")) @QtCore.pyqtSlot(ui_pb2.NotificationReply) def _cb_notification_callback(self, reply): if reply.id in self._notifications_sent: noti = self._notifications_sent[reply.id] if reply.code == ui_pb2.ERROR: self._show_message(QtCore.QCoreApplication.translate("proc_details", "<b>Error loading process information:</b> <br><br>\n\n") + reply.data) self._pid = "" self._set_button_running(False) # if we haven't loaded any data yet, just close the window if self._data_loaded == False: # but if there're more than 1 pid keep the window open. # we may have one pid already closed and one alive. if self.comboPids.count() <= 1: self._close() self._delete_notification(reply.id) return if noti.type == ui_pb2.MONITOR_PROCESS and reply.data != "": self._load_data(reply.data) elif noti.type == ui_pb2.STOP_MONITOR_PROCESS: if reply.data != "": self.show_message(QtCore.QCoreApplication.translate("proc_details", "<b>Error stopping monitoring process:</b><br><br>") + reply.data) self._set_button_running(False) self._delete_notification(reply.id) else: print("[stats] unknown notification received: ", reply.id) def closeEvent(self, e): self._close() def _cb_close_clicked(self): self._close() def _cb_combo_pids_changed(self, idx): if idx == -1: return # TODO: this event causes to send to 2 Start notifications #if self._pid != "" and self._pid != self.comboPids.currentText(): # self._stop_monitoring() # self._pid = self.comboPids.currentText() # self._start_monitoring() def _cb_action_clicked(self): if not self.cmdAction.isChecked(): self._stop_monitoring() else: self._start_monitoring() def _show_message(self, text): Message.ok(text, "", QtWidgets.QMessageBox.Warning) def _delete_notification(self, nid): if nid in self._notifications_sent: del self._notifications_sent[nid] def _reset(self): self._app_name = None self._app_icon = None self.comboPids.clear() self.labelProcName.setText(QtCore.QCoreApplication.translate("proc_details", "loading...")) self.labelProcArgs.setText(QtCore.QCoreApplication.translate("proc_details", "loading...")) self.labelProcIcon.clear() self.labelStatm.setText("") self.labelCwd.setText("") for tidx in range(0, len(self.TABS)): self.TABS[tidx]['text'].setPlainText("") def _set_button_running(self, yes): if yes: self.cmdAction.setChecked(True) self.cmdAction.setIcon(self.iconPause) else: self.cmdAction.setChecked(False) self.cmdAction.setIcon(self.iconStart) def _close(self): self._stop_monitoring() self.comboPids.clear() self._pid = "" self.hide() def monitor(self, pids): if self._pid != "": self._stop_monitoring() self._data_loaded = False self._pids = pids self._reset() for pid in pids: self.comboPids.addItem(pid) self.show() self._start_monitoring() def _set_tab_text(self, tab_idx, text): self.TABS[tab_idx]['scrollPos'] = self.TABS[tab_idx]['text'].verticalScrollBar().value() self.TABS[tab_idx]['text'].setPlainText(text) self.TABS[tab_idx]['text'].verticalScrollBar().setValue(self.TABS[tab_idx]['scrollPos']) def _start_monitoring(self): try: # avoid to send notifications without a pid if self._pid != "": return self._pid = self.comboPids.currentText() if self._pid == "": return self._set_button_running(True) noti = ui_pb2.Notification(clientName="", serverName="", type=ui_pb2.MONITOR_PROCESS, data=self._pid, rules=[]) self._nid = self._nodes.send_notification(self._pids[self._pid], noti, self._notification_callback) self._notifications_sent[self._nid] = noti except Exception as e: print(self.LOG_TAG + "exception starting monitoring: ", e) def _stop_monitoring(self): if self._pid == "": return self._set_button_running(False) noti = ui_pb2.Notification(clientName="", serverName="", type=ui_pb2.STOP_MONITOR_PROCESS, data=str(self._pid), rules=[]) self._nid = self._nodes.send_notification(self._pids[self._pid], noti, self._notification_callback) self._notifications_sent[self._nid] = noti self._pid = "" self._app_icon = None def _load_data(self, data): tab_idx = self.tabWidget.currentIndex() try: proc = json.loads(data) self._load_app_icon(proc['Path']) if self._app_name != None: self.labelProcName.setText("<b>" + self._app_name + "</b>") self.labelProcName.setToolTip("<b>" + self._app_name + "</b>") #if proc['Path'] not in proc['Args']: # proc['Args'].insert(0, proc['Path']) self.labelProcArgs.setFixedHeight(30) self.labelProcArgs.setText(" ".join(proc['Args'])) self.labelProcArgs.setToolTip(" ".join(proc['Args'])) self.labelCwd.setText("<b>CWD: </b>" + proc['CWD']) self.labelCwd.setToolTip("<b>CWD: </b>" + proc['CWD']) self._load_mem_data(proc['Statm']) if tab_idx == self.TAB_STATUS: self._set_tab_text(tab_idx, proc['Status']) elif tab_idx == self.TAB_DESCRIPTORS: self._load_descriptors(proc['Descriptors']) elif tab_idx == self.TAB_IOSTATS: self._load_iostats(proc['IOStats']) elif tab_idx == self.TAB_MAPS: self._set_tab_text(tab_idx, proc['Maps']) elif tab_idx == self.TAB_STACK: self._set_tab_text(tab_idx, proc['Stack']) elif tab_idx == self.TAB_ENVS: self._load_env_vars(proc['Env']) self._data_loaded = True except Exception as e: print(self.LOG_TAG + "exception loading data: ", e) def _load_app_icon(self, proc_path): if self._app_icon != None: return self._app_name, self._app_icon, _, _ = self._apps_parser.get_info_by_path(proc_path, "terminal") icon = QtGui.QIcon().fromTheme(self._app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) self.labelProcIcon.setPixmap(pixmap) if self._app_name == None: self._app_name = proc_path def _load_iostats(self, iostats): ioText = "%-16s %dMB<br>%-16s %dMB<br>%-16s %d<br>%-16s %d<br>%-16s %dMB<br>%-16s %dMB<br>" % ( "<b>Chars read:</b>", ((iostats['RChar'] / 1024) / 1024), "<b>Chars written:</b>", ((iostats['WChar'] / 1024) / 1024), "<b>Syscalls read:</b>", (iostats['SyscallRead']), "<b>Syscalls write:</b>", (iostats['SyscallWrite']), "<b>KB read:</b>", ((iostats['ReadBytes'] / 1024) / 1024), "<b>KB written: </b>", ((iostats['WriteBytes'] / 1024) / 1024) ) self.textIOStats.setPlainText("") self.textIOStats.appendHtml(ioText) def _load_mem_data(self, mem): # assuming page size == 4096 pagesize = 4096 memText = "<b>VIRT:</b> %dMB, <b>RSS:</b> %dMB, <b>Libs:</b> %dMB, <b>Data:</b> %dMB, <b>Text:</b> %dMB" % ( ((mem['Size'] * pagesize) / 1024) / 1024, ((mem['Resident'] * pagesize) / 1024) / 1024, ((mem['Lib'] * pagesize) / 1024) / 1024, ((mem['Data'] * pagesize) / 1024) / 1024, ((mem['Text'] * pagesize) / 1024) / 1024 ) self.labelStatm.setText(memText) def _load_descriptors(self, descriptors): text = "%-12s%-40s%-8s -> %s\n\n" % ("Size", "Time", "Name", "Symlink") for d in descriptors: text += "{:<12}{:<40}{:<8} -> {}\n".format(str(d['Size']), d['ModTime'], d['Name'], d['SymLink']) self._set_tab_text(self.TAB_DESCRIPTORS, text) def _load_env_vars(self, envs): if envs == {}: self._set_tab_text(self.TAB_ENVS, "<no environment variables>") return text = "%-15s\t%s\n\n" % ("Name", "Value") for env_name in envs: text += "%-15s:\t%s\n" % (env_name, envs[env_name]) self._set_tab_text(self.TAB_ENVS, text)
class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _prompt_trigger = QtCore.pyqtSignal() _tick_trigger = QtCore.pyqtSignal() _timeout_trigger = QtCore.pyqtSignal() DEFAULT_TIMEOUT = 15 ACTION_ALLOW = "allow" ACTION_DENY = "deny" FIELD_REGEX_HOST = "regex_host" FIELD_REGEX_IP = "regex_ip" FIELD_PROC_PATH = "process_path" FIELD_PROC_ARGS = "process_args" FIELD_USER_ID = "user_id" FIELD_DST_IP = "dst_ip" FIELD_DST_PORT = "dst_port" FIELD_DST_NETWORK = "dst_network" FIELD_DST_HOST = "simple_host" DURATION_once = "once" DURATION_30s = "30s" DURATION_5m = "5m" DURATION_15m = "15m" DURATION_30m = "30m" DURATION_1h = "1h" # label displayed in the pop-up combo DURATION_session = "for this session" # field of a rule DURATION_restart = "until restart" # label displayed in the pop-up combo DURATION_forever = "forever" # field of a rule DURATION_always = "always" CFG_DEFAULT_TIMEOUT = "global/default_timeout" CFG_DEFAULT_ACTION = "global/default_action" def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) # Other interesting flags: QtCore.Qt.Tool | QtCore.Qt.BypassWindowManagerHint self._cfg = Config.get() self.setupUi(self) dialog_geometry = self._cfg.getSettings("promptDialog/geometry") if dialog_geometry == QtCore.QByteArray: self.restoreGeometry(dialog_geometry) self.setWindowTitle("OpenSnitch v%s" % version) self._lock = threading.Lock() self._con = None self._rule = None self._local = True self._peer = None self._prompt_trigger.connect(self.on_connection_prompt_triggered) self._timeout_trigger.connect(self.on_timeout_triggered) self._tick_trigger.connect(self.on_tick_triggered) self._tick = int(self._cfg.getSettings( self.CFG_DEFAULT_TIMEOUT)) if self._cfg.hasKey( self.CFG_DEFAULT_TIMEOUT) else self.DEFAULT_TIMEOUT self._tick_thread = None self._done = threading.Event() self._timeout_text = "" self._timeout_triggered = False self._apps_parser = LinuxDesktopParser() self.denyButton.clicked.connect(self._on_deny_clicked) # also accept button self.applyButton.clicked.connect(self._on_apply_clicked) self._apply_text = "Allow" self._deny_text = "Deny" self._default_action = self._cfg.getSettings(self.CFG_DEFAULT_ACTION) self.whatIPCombo.setVisible(False) self.checkDstIP.setVisible(False) self.checkDstPort.setVisible(False) self.checkUserID.setVisible(False) self._ischeckAdvanceded = False self.checkAdvanced.toggled.connect(self._checkbox_toggled) if QtGui.QIcon.hasThemeIcon("emblem-default") == False: self.applyButton.setIcon(self.style().standardIcon( getattr(QtWidgets.QStyle, "SP_DialogApplyButton"))) self.denyButton.setIcon(self.style().standardIcon( getattr(QtWidgets.QStyle, "SP_DialogCancelButton"))) def showEvent(self, event): super(PromptDialog, self).showEvent(event) self.resize(540, 300) self.activateWindow() def _checkbox_toggled(self, state): self.applyButton.setText("%s" % self._apply_text) self.denyButton.setText("%s" % self._deny_text) self._tick_thread.stop = state self.checkDstIP.setVisible(state) self.whatIPCombo.setVisible(state) self.destIPLabel.setVisible(not state) self.checkDstPort.setVisible(state) self.checkUserID.setVisible(state) self._ischeckAdvanceded = state def _set_elide_text(self, widget, text, max_size=132): if len(text) > max_size: text = text[:max_size] + "..." widget.setText(text) def promptUser(self, connection, is_local, peer): # one at a time with self._lock: # reset state if self._tick_thread != None and self._tick_thread.is_alive(): self._tick_thread.join() self._cfg.reload() self._tick = int(self._cfg.getSettings( self.CFG_DEFAULT_TIMEOUT)) if self._cfg.hasKey( self.CFG_DEFAULT_TIMEOUT) else self.DEFAULT_TIMEOUT self._tick_thread = threading.Thread(target=self._timeout_worker) self._tick_thread.stop = self._ischeckAdvanceded self._timeout_triggered = False self._rule = None self._local = is_local self._peer = peer self._con = connection self._done.clear() # trigger and show dialog self._prompt_trigger.emit() # start timeout thread self._tick_thread.start() # wait for user choice or timeout self._done.wait() return self._rule, self._timeout_triggered def _timeout_worker(self): if self._tick == 0: self._timeout_trigger.emit() return while self._tick > 0 and self._done.is_set() is False: t = threading.currentThread() # stop only stops the coundtdown, not the thread itself. if getattr(t, "stop", True): self._tick = int( self._cfg.getSettings(self.CFG_DEFAULT_TIMEOUT)) time.sleep(1) continue self._tick -= 1 self._tick_trigger.emit() time.sleep(1) if not self._done.is_set(): self._timeout_trigger.emit() @QtCore.pyqtSlot() def on_connection_prompt_triggered(self): self._render_connection(self._con) if self._tick > 0: self.show() @QtCore.pyqtSlot() def on_tick_triggered(self): if self._cfg.getSettings(self.CFG_DEFAULT_ACTION) == self.ACTION_ALLOW: self._timeout_text = "%s (%d)" % (self._apply_text, self._tick) self.applyButton.setText(self._timeout_text) else: self._timeout_text = "%s (%d)" % (self._deny_text, self._tick) self.denyButton.setText(self._timeout_text) @QtCore.pyqtSlot() def on_timeout_triggered(self): self._timeout_triggered = True self._send_rule() def _configure_default_duration(self): if self._cfg.getSettings( "global/default_duration") == self.DURATION_once: self.durationCombo.setCurrentIndex(0) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_30s: self.durationCombo.setCurrentIndex(1) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_5m: self.durationCombo.setCurrentIndex(2) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_15m: self.durationCombo.setCurrentIndex(3) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_30m: self.durationCombo.setCurrentIndex(4) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_1h: self.durationCombo.setCurrentIndex(5) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_session: self.durationCombo.setCurrentIndex(6) elif self._cfg.getSettings( "global/default_duration") == self.DURATION_forever: self.durationCombo.setCurrentIndex(7) else: # default to "for this session" self.durationCombo.setCurrentIndex(6) def _set_cmd_action_text(self): if self._cfg.getSettings(self.CFG_DEFAULT_ACTION) == self.ACTION_ALLOW: self.applyButton.setText("%s (%d)" % (self._apply_text, self._tick)) self.denyButton.setText(self._deny_text) else: self.denyButton.setText("%s (%d)" % (self._deny_text, self._tick)) self.applyButton.setText(self._apply_text) self.checkAdvanced.setFocus() def _render_connection(self, con): app_name, app_icon, _ = self._apps_parser.get_info_by_path( con.process_path, "terminal") if app_name != con.process_path and len( con.process_args ) > 1 and con.process_path not in con.process_args: self.appPathLabel.setToolTip("Process path: %s" % con.process_path) self._set_elide_text(self.appPathLabel, "(%s)" % con.process_path) else: self.appPathLabel.setFixedHeight(1) self.appPathLabel.setText("") if app_name == "": app_name = "Unknown process" self.appNameLabel.setText("Outgoing connection") else: self.appNameLabel.setText(app_name) self.appNameLabel.setToolTip(app_name) self.cwdLabel.setToolTip("Process launched from: %s" % con.process_cwd) self._set_elide_text(self.cwdLabel, con.process_cwd, max_size=32) icon = QtGui.QIcon().fromTheme(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) self.iconLabel.setPixmap(pixmap) if self._local: message = "<b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, con.dst_host or con.dst_ip, con.protocol, con.dst_port ) else: message = "<b>Remote</b> process <b>%s</b> running on <b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, self._peer.split(':')[1], con.dst_host or con.dst_ip, con.protocol, con.dst_port ) self.messageLabel.setText(message) self.messageLabel.setToolTip(message) self.sourceIPLabel.setText(con.src_ip) self.destIPLabel.setText(con.dst_ip) self.destPortLabel.setText(str(con.dst_port)) if self._local: try: uid = "%d (%s)" % (con.user_id, pwd.getpwuid( con.user_id).pw_name) except: uid = "" else: uid = "%d" % con.user_id self.uidLabel.setText(uid) self.pidLabel.setText("%s" % con.process_id) self._set_elide_text(self.argsLabel, ' '.join(con.process_args)) self.argsLabel.setToolTip(' '.join(con.process_args)) self.whatCombo.clear() self.whatIPCombo.clear() if int(con.process_id) > 0: self.whatCombo.addItem("from this executable", self.FIELD_PROC_PATH) self.whatCombo.addItem("from this command line", self.FIELD_PROC_ARGS) if self.argsLabel.text() == "": self._set_elide_text(self.argsLabel, con.process_path) # the order of the entries must match those in the preferences dialog # prefs -> UI -> Default target self.whatCombo.addItem("to port %d" % con.dst_port, self.FIELD_DST_PORT) self.whatCombo.addItem("to %s" % con.dst_ip, self.FIELD_DST_IP) if int(con.user_id) >= 0: self.whatCombo.addItem("from user %s" % uid, self.FIELD_USER_ID) self._add_dst_networks_to_combo(self.whatCombo, con.dst_ip) if con.dst_host != "" and con.dst_host != con.dst_ip: self._add_dsthost_to_combo(con.dst_host) self.whatIPCombo.addItem("to %s" % con.dst_ip, self.FIELD_DST_IP) parts = con.dst_ip.split('.') nparts = len(parts) for i in range(1, nparts): self.whatCombo.addItem("to %s.*" % '.'.join(parts[:i]), self.FIELD_REGEX_IP) self.whatIPCombo.addItem("to %s.*" % '.'.join(parts[:i]), self.FIELD_REGEX_IP) self._add_dst_networks_to_combo(self.whatIPCombo, con.dst_ip) self._default_action = self._cfg.getSettings(self.CFG_DEFAULT_ACTION) self._configure_default_duration() if int(con.process_id) > 0: self.whatCombo.setCurrentIndex( int(self._cfg.getSettings("global/default_target"))) else: self.whatCombo.setCurrentIndex(2) self._set_cmd_action_text() self.setFixedSize(self.size()) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): if not event.key() == QtCore.Qt.Key_Escape: super(PromptDialog, self).keyPressEvent(event) # prevent a click on the window's x # from quitting the whole application def closeEvent(self, e): self._send_rule() e.ignore() def _add_dst_networks_to_combo(self, combo, dst_ip): if type(ipaddress.ip_address(dst_ip)) == ipaddress.IPv4Address: combo.addItem( "to %s" % ipaddress.ip_network(dst_ip + "/24", strict=False), self.FIELD_DST_NETWORK) combo.addItem( "to %s" % ipaddress.ip_network(dst_ip + "/16", strict=False), self.FIELD_DST_NETWORK) combo.addItem( "to %s" % ipaddress.ip_network(dst_ip + "/8", strict=False), self.FIELD_DST_NETWORK) else: combo.addItem( "to %s" % ipaddress.ip_network(dst_ip + "/64", strict=False), self.FIELD_DST_NETWORK) combo.addItem( "to %s" % ipaddress.ip_network(dst_ip + "/128", strict=False), self.FIELD_DST_NETWORK) def _add_dsthost_to_combo(self, dst_host): self.whatCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST) self.whatIPCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST) parts = dst_host.split('.')[1:] nparts = len(parts) for i in range(0, nparts - 1): self.whatCombo.addItem("to *.%s" % '.'.join(parts[i:]), self.FIELD_REGEX_HOST) self.whatIPCombo.addItem("to *.%s" % '.'.join(parts[i:]), self.FIELD_REGEX_HOST) if nparts == 1: self.whatCombo.addItem("to *%s" % dst_host, self.FIELD_REGEX_HOST) self.whatIPCombo.addItem("to *%s" % dst_host, self.FIELD_REGEX_HOST) def _get_duration(self, duration_idx): if duration_idx == 0: return self.DURATION_once elif duration_idx == 1: return self.DURATION_30s elif duration_idx == 2: return self.DURATION_5m elif duration_idx == 3: return self.DURATION_15m elif duration_idx == 4: return self.DURATION_30m elif duration_idx == 5: return self.DURATION_1h elif duration_idx == 6: return self.DURATION_restart else: return self.DURATION_always def _get_combo_operator(self, combo, what_idx): if combo.itemData(what_idx) == self.FIELD_PROC_PATH: return "simple", "process.path", self._con.process_path elif combo.itemData(what_idx) == self.FIELD_PROC_ARGS: return "simple", "process.command", ' '.join( self._con.process_args) elif combo.itemData(what_idx) == self.FIELD_USER_ID: return "simple", "user.id", "%s" % self._con.user_id elif combo.itemData(what_idx) == self.FIELD_DST_PORT: return "simple", "dest.port", "%s" % self._con.dst_port elif combo.itemData(what_idx) == self.FIELD_DST_IP: return "simple", "dest.ip", self._con.dst_ip elif combo.itemData(what_idx) == self.FIELD_DST_HOST: return "simple", "dest.host", combo.currentText() elif combo.itemData(what_idx) == self.FIELD_DST_NETWORK: # strip "to ": "to x.x.x/20" -> "x.x.x/20" return "network", "dest.network", combo.currentText()[3:] elif combo.itemData(what_idx) == self.FIELD_REGEX_HOST: return "regexp", "dest.host", "%s" % '\.'.join( combo.currentText().split('.')).replace("*", ".*")[3:] elif combo.itemData(what_idx) == self.FIELD_REGEX_IP: return "regexp", "dest.ip", "%s" % '\.'.join( combo.currentText().split('.')).replace("*", ".*")[3:] def _on_deny_clicked(self): self._default_action = self.ACTION_DENY self._send_rule() def _on_apply_clicked(self): self._default_action = self.ACTION_ALLOW self._send_rule() def _get_rule_name(self, rule): rule_temp_name = slugify("%s %s" % (rule.action, rule.duration)) if self._ischeckAdvanceded: rule_temp_name = "%s-list" % rule_temp_name else: rule_temp_name = "%s-simple" % rule_temp_name rule_temp_name = slugify("%s %s" % (rule_temp_name, rule.operator.data)) return rule_temp_name[:128] def _send_rule(self): self._cfg.setSettings("promptDialog/geometry", self.saveGeometry()) self._rule = ui_pb2.Rule(name="user.choice") self._rule.enabled = True self._rule.action = self._default_action self._rule.duration = self._get_duration( self.durationCombo.currentIndex()) what_idx = self.whatCombo.currentIndex() self._rule.operator.type, self._rule.operator.operand, self._rule.operator.data = self._get_combo_operator( self.whatCombo, what_idx) if self._rule.operator.data == "": print("Invalid rule, discarding: ", self._rule) self._rule = None self._done.set() return rule_temp_name = self._get_rule_name(self._rule) self._rule.name = rule_temp_name # TODO: move to a method data = [] if self._ischeckAdvanceded and self.checkDstIP.isChecked( ) and self.whatCombo.itemData(what_idx) != self.FIELD_DST_IP: _type, _operand, _data = self._get_combo_operator( self.whatIPCombo, self.whatIPCombo.currentIndex()) data.append({"type": _type, "operand": _operand, "data": _data}) rule_temp_name = slugify("%s %s" % (rule_temp_name, _data)) if self._ischeckAdvanceded and self.checkDstPort.isChecked( ) and self.whatCombo.itemData(what_idx) != self.FIELD_DST_PORT: data.append({ "type": "simple", "operand": "dest.port", "data": str(self._con.dst_port) }) rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.dst_port))) if self._ischeckAdvanceded and self.checkUserID.isChecked( ) and self.whatCombo.itemData(what_idx) != self.FIELD_USER_ID: data.append({ "type": "simple", "operand": "user.id", "data": str(self._con.user_id) }) rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.user_id))) if self._ischeckAdvanceded: data.append({ "type": self._rule.operator.type, "operand": self._rule.operator.operand, "data": self._rule.operator.data }) self._rule.operator.data = json.dumps(data) self._rule.operator.type = "list" self._rule.operator.operand = "" self._rule.name = rule_temp_name self.hide() if self._ischeckAdvanceded: self.checkAdvanced.toggle() self._ischeckAdvanceded = False # signal that the user took a decision and # a new rule is available self._done.set()
class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _prompt_trigger = QtCore.pyqtSignal() _tick_trigger = QtCore.pyqtSignal() _timeout_trigger = QtCore.pyqtSignal() DEFAULT_TIMEOUT = 15 ACTION_IDX_DENY = 0 ACTION_IDX_ALLOW = 1 FIELD_REGEX_HOST = "regex_host" FIELD_REGEX_IP = "regex_ip" FIELD_PROC_PATH = "process_path" FIELD_PROC_ARGS = "process_args" FIELD_USER_ID = "user_id" FIELD_DST_IP = "dst_ip" FIELD_DST_PORT = "dst_port" FIELD_DST_NETWORK = "dst_network" FIELD_DST_HOST = "simple_host" # don't translate DURATION_30s = "30s" DURATION_5m = "5m" DURATION_15m = "15m" DURATION_30m = "30m" DURATION_1h = "1h" # don't translate # label displayed in the pop-up combo DURATION_session = QC.translate("popups", "until reboot") # label displayed in the pop-up combo DURATION_forever = QC.translate("popups", "forever") def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) # Other interesting flags: QtCore.Qt.Tool | QtCore.Qt.BypassWindowManagerHint self._cfg = Config.get() self.setupUi(self) dialog_geometry = self._cfg.getSettings("promptDialog/geometry") if dialog_geometry == QtCore.QByteArray: self.restoreGeometry(dialog_geometry) self.setWindowTitle("OpenSnitch v%s" % version) self._lock = threading.Lock() self._con = None self._rule = None self._local = True self._peer = None self._prompt_trigger.connect(self.on_connection_prompt_triggered) self._timeout_trigger.connect(self.on_timeout_triggered) self._tick_trigger.connect(self.on_tick_triggered) self._tick = int(self._cfg.getSettings(self._cfg.DEFAULT_TIMEOUT_KEY)) if self._cfg.hasKey(self._cfg.DEFAULT_TIMEOUT_KEY) else self.DEFAULT_TIMEOUT self._tick_thread = None self._done = threading.Event() self._timeout_text = "" self._timeout_triggered = False self._apps_parser = LinuxDesktopParser() self.denyButton.clicked.connect(self._on_deny_clicked) # also accept button self.applyButton.clicked.connect(self._on_apply_clicked) self._apply_text = QC.translate("popups", "Allow") self._deny_text = QC.translate("popups", "Deny") self._default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY) self.whatIPCombo.setVisible(False) self.checkDstIP.setVisible(False) self.checkDstPort.setVisible(False) self.checkUserID.setVisible(False) self.appDescriptionLabel.setVisible(False) self._ischeckAdvanceded = False self.checkAdvanced.toggled.connect(self._checkbox_toggled) if QtGui.QIcon.hasThemeIcon("emblem-default") == False: self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton"))) self.denyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCancelButton"))) def showEvent(self, event): super(PromptDialog, self).showEvent(event) self.resize(540, 300) self.activateWindow() def _checkbox_toggled(self, state): self.applyButton.setText("%s" % self._apply_text) self.denyButton.setText("%s" % self._deny_text) self._tick_thread.stop = state self.checkDstIP.setVisible(state) self.whatIPCombo.setVisible(state) self.destIPLabel.setVisible(not state) self.checkDstPort.setVisible(state) self.checkUserID.setVisible(state) self._ischeckAdvanceded = state def _set_elide_text(self, widget, text, max_size=132): if len(text) > max_size: text = text[:max_size] + "..." widget.setText(text) def promptUser(self, connection, is_local, peer): # one at a time with self._lock: # reset state if self._tick_thread != None and self._tick_thread.is_alive(): self._tick_thread.join() self._cfg.reload() self._tick = int(self._cfg.getSettings(self._cfg.DEFAULT_TIMEOUT_KEY)) if self._cfg.hasKey(self._cfg.DEFAULT_TIMEOUT_KEY) else self.DEFAULT_TIMEOUT self._tick_thread = threading.Thread(target=self._timeout_worker) self._tick_thread.stop = self._ischeckAdvanceded self._timeout_triggered = False self._rule = None self._local = is_local self._peer = peer self._con = connection self._done.clear() # trigger and show dialog self._prompt_trigger.emit() # start timeout thread self._tick_thread.start() # wait for user choice or timeout self._done.wait() return self._rule, self._timeout_triggered def _timeout_worker(self): if self._tick == 0: self._timeout_trigger.emit() return while self._tick > 0 and self._done.is_set() is False: t = threading.currentThread() # stop only stops the coundtdown, not the thread itself. if getattr(t, "stop", True): self._tick = int(self._cfg.getSettings(self._cfg.DEFAULT_TIMEOUT_KEY)) time.sleep(1) continue self._tick -= 1 self._tick_trigger.emit() time.sleep(1) if not self._done.is_set(): self._timeout_trigger.emit() @QtCore.pyqtSlot() def on_connection_prompt_triggered(self): self._render_connection(self._con) if self._tick > 0: self.show() @QtCore.pyqtSlot() def on_tick_triggered(self): self._set_cmd_action_text() @QtCore.pyqtSlot() def on_timeout_triggered(self): self._timeout_triggered = True self._send_rule() def _configure_default_duration(self): if self._cfg.hasKey(self._cfg.DEFAULT_DURATION_KEY): cur_idx = self._cfg.getInt(self._cfg.DEFAULT_DURATION_KEY) self.durationCombo.setCurrentIndex(cur_idx) else: self.durationCombo.setCurrentIndex(self._cfg.DEFAULT_DURATION_IDX) def _set_cmd_action_text(self): if self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY) == self.ACTION_IDX_ALLOW: self.applyButton.setText("%s (%d)" % (self._apply_text, self._tick)) self.denyButton.setText(self._deny_text) else: self.denyButton.setText("%s (%d)" % (self._deny_text, self._tick)) self.applyButton.setText(self._apply_text) def _set_app_description(self, description): if description != None: self.appDescriptionLabel.setVisible(True) self.appDescriptionLabel.setToolTip(description) self._set_elide_text(self.appDescriptionLabel, "%s" % description) else: self.appDescriptionLabel.setVisible(False) self.appDescriptionLabel.setText("") def _set_app_path(self, app_name, app_args, con): # show the binary path if it's not part of the cmdline args: # cmdline: telnet 1.1.1.1 (path: /usr/bin/telnet.netkit) # cmdline: /usr/bin/telnet.netkit 1.1.1.1 (the binary path is part of the cmdline args, no need to display it) if con.process_path != "" and len(con.process_args) >= 1 and con.process_path not in con.process_args: self.appPathLabel.setToolTip("Process path: %s" % con.process_path) if app_name.lower() == app_args: self._set_elide_text(self.appPathLabel, "%s" % con.process_path) else: self._set_elide_text(self.appPathLabel, "(%s)" % con.process_path) self.appPathLabel.setVisible(True) else: self.appPathLabel.setVisible(False) self.appPathLabel.setText("") def _set_app_args(self, app_name, app_args): # if the app name and the args are the same, there's no need to display # the args label (amule for example) if app_name.lower() != app_args: self.argsLabel.setVisible(True) self._set_elide_text(self.argsLabel, app_args) self.argsLabel.setToolTip(app_args) else: self.argsLabel.setVisible(False) self.argsLabel.setText("") def _render_connection(self, con): app_name, app_icon, description, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal") app_args = " ".join(con.process_args) self._set_app_description(description) self._set_app_path(app_name, app_args, con) self._set_app_args(app_name, app_args) if app_name == "": self.appPathLabel.setVisible(False) self.argsLabel.setVisible(False) app_name = QC.translate("popups", "Unknown process: %s" % con.process_path) self.appNameLabel.setText(QC.translate("popups", "Outgoing connection")) else: self.appNameLabel.setText(app_name) self.appNameLabel.setToolTip(app_name) self.cwdLabel.setToolTip("%s %s" % (QC.translate("popups", "Process launched from:"), con.process_cwd)) self._set_elide_text(self.cwdLabel, con.process_cwd, max_size=32) pixmap = self._get_app_icon(app_icon) self.iconLabel.setPixmap(pixmap) message = self._get_popup_message(app_name, con) self.messageLabel.setText(message) self.messageLabel.setToolTip(message) self.sourceIPLabel.setText(con.src_ip) self.destIPLabel.setText(con.dst_ip) self.destPortLabel.setText(str(con.dst_port)) if self._local: try: uid = "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name) except: uid = "" else: uid = "%d" % con.user_id self.uidLabel.setText(uid) self.pidLabel.setText("%s" % con.process_id) self.whatCombo.clear() self.whatIPCombo.clear() if int(con.process_id) > 0: self.whatCombo.addItem(QC.translate("popups", "from this executable"), self.FIELD_PROC_PATH) self.whatCombo.addItem(QC.translate("popups", "from this command line"), self.FIELD_PROC_ARGS) # the order of the entries must match those in the preferences dialog # prefs -> UI -> Default target self.whatCombo.addItem(QC.translate("popups", "to port {0}").format(con.dst_port), self.FIELD_DST_PORT) self.whatCombo.addItem(QC.translate("popups", "to {0}").format(con.dst_ip), self.FIELD_DST_IP) if int(con.user_id) >= 0: self.whatCombo.addItem(QC.translate("popups", "from user {0}").format(uid), self.FIELD_USER_ID) self._add_dst_networks_to_combo(self.whatCombo, con.dst_ip) if con.dst_host != "" and con.dst_host != con.dst_ip: self._add_dsthost_to_combo(con.dst_host) self.whatIPCombo.addItem(QC.translate("popups", "to {0}").format(con.dst_ip), self.FIELD_DST_IP) parts = con.dst_ip.split('.') nparts = len(parts) for i in range(1, nparts): self.whatCombo.addItem(QC.translate("popups", "to {0}.*").format('.'.join(parts[:i])), self.FIELD_REGEX_IP) self.whatIPCombo.addItem(QC.translate("popups", "to {0}.*").format( '.'.join(parts[:i])), self.FIELD_REGEX_IP) self._add_dst_networks_to_combo(self.whatIPCombo, con.dst_ip) self._default_action = self._cfg.getInt(self._cfg.DEFAULT_ACTION_KEY) self._configure_default_duration() if int(con.process_id) > 0: self.whatCombo.setCurrentIndex(int(self._cfg.getSettings(self._cfg.DEFAULT_TARGET_KEY))) else: self.whatCombo.setCurrentIndex(2) self._set_cmd_action_text() self.checkAdvanced.setFocus() self.setFixedSize(self.size()) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): if not event.key() == QtCore.Qt.Key_Escape: super(PromptDialog, self).keyPressEvent(event) # prevent a click on the window's x # from quitting the whole application def closeEvent(self, e): self._send_rule() e.ignore() def _add_dst_networks_to_combo(self, combo, dst_ip): if type(ipaddress.ip_address(dst_ip)) == ipaddress.IPv4Address: combo.addItem(QC.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/24", strict=False)), self.FIELD_DST_NETWORK) combo.addItem(QC.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/16", strict=False)), self.FIELD_DST_NETWORK) combo.addItem(QC.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/8", strict=False)), self.FIELD_DST_NETWORK) else: combo.addItem(QC.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/64", strict=False)), self.FIELD_DST_NETWORK) combo.addItem(QC.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/128", strict=False)), self.FIELD_DST_NETWORK) def _add_dsthost_to_combo(self, dst_host): self.whatCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST) self.whatIPCombo.addItem("%s" % dst_host, self.FIELD_DST_HOST) parts = dst_host.split('.')[1:] nparts = len(parts) for i in range(0, nparts - 1): self.whatCombo.addItem(QC.translate("popups", "to *.{0}").format('.'.join(parts[i:])), self.FIELD_REGEX_HOST) self.whatIPCombo.addItem(QC.translate("popups", "to *.{0}").format('.'.join(parts[i:])), self.FIELD_REGEX_HOST) if nparts == 1: self.whatCombo.addItem(QC.translate("popups", "to *{0}").format(dst_host), self.FIELD_REGEX_HOST) self.whatIPCombo.addItem(QC.translate("popups", "to *{0}").format(dst_host), self.FIELD_REGEX_HOST) def _get_app_icon(self, app_icon): """we try to get the icon of an app from the system. If it's not found, then we'll try to search for it in common directories of the system. """ icon = QtGui.QIcon().fromTheme(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) if QtGui.QIcon().hasThemeIcon(app_icon) == False or pixmap.height() == 0: # sometimes the icon is an absolute path, sometimes it's not if os.path.isabs(app_icon): icon = QtGui.QIcon(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) else: icon_path = self._apps_parser.discover_app_icon(app_icon) if icon_path != None: icon = QtGui.QIcon(icon_path) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) return pixmap def _get_popup_message(self, app_name, con): """ _get_popup_message helps constructing the message that is displayed on the pop-up dialog. Example: curl is connecting to www.opensnitch.io on TCP port 443 """ message = "<b>%s</b>" % app_name if not self._local: message = QC.translate("popups", "<b>Remote</b> process %s running on <b>%s</b>") % ( \ message, self._peer.split(':')[1]) msg_action = QC.translate("popups", "is connecting to <b>%s</b> on %s port %d") % ( \ con.dst_host or con.dst_ip, con.protocol.upper(), con.dst_port ) if con.dst_port == 53 and con.dst_ip != con.dst_host and con.dst_host != "": msg_action = QC.translate("popups", "is attempting to resolve <b>%s</b> via %s, %s port %d") % ( \ con.dst_host, con.dst_ip, con.protocol.upper(), con.dst_port) return "%s %s" % (message, msg_action) def _get_duration(self, duration_idx): if duration_idx == 0: return Config.DURATION_ONCE elif duration_idx == 1: return self.DURATION_30s elif duration_idx == 2: return self.DURATION_5m elif duration_idx == 3: return self.DURATION_15m elif duration_idx == 4: return self.DURATION_30m elif duration_idx == 5: return self.DURATION_1h elif duration_idx == 6: return Config.DURATION_UNTIL_RESTART else: return Config.DURATION_ALWAYS def _get_combo_operator(self, combo, what_idx): if combo.itemData(what_idx) == self.FIELD_PROC_PATH: return "simple", "process.path", self._con.process_path elif combo.itemData(what_idx) == self.FIELD_PROC_ARGS: return "simple", "process.command", ' '.join(self._con.process_args) elif combo.itemData(what_idx) == self.FIELD_USER_ID: return "simple", "user.id", "%s" % self._con.user_id elif combo.itemData(what_idx) == self.FIELD_DST_PORT: return "simple", "dest.port", "%s" % self._con.dst_port elif combo.itemData(what_idx) == self.FIELD_DST_IP: return "simple", "dest.ip", self._con.dst_ip elif combo.itemData(what_idx) == self.FIELD_DST_HOST: return "simple", "dest.host", combo.currentText() elif combo.itemData(what_idx) == self.FIELD_DST_NETWORK: # strip "to ": "to x.x.x/20" -> "x.x.x/20" # we assume that to is one word in all languages parts = combo.currentText().split(' ') text = parts[len(parts)-1] return "network", "dest.network", text elif combo.itemData(what_idx) == self.FIELD_REGEX_HOST: parts = combo.currentText().split(' ') text = parts[len(parts)-1] return "regexp", "dest.host", "%s" % '\.'.join(text.split('.')).replace("*", ".*") elif combo.itemData(what_idx) == self.FIELD_REGEX_IP: parts = combo.currentText().split(' ') text = parts[len(parts)-1] return "regexp", "dest.ip", "%s" % '\.'.join(text.split('.')).replace("*", ".*") def _on_deny_clicked(self): self._default_action = self.ACTION_IDX_DENY self._send_rule() def _on_apply_clicked(self): self._default_action = self.ACTION_IDX_ALLOW self._send_rule() def _get_rule_name(self, rule): rule_temp_name = slugify("%s %s" % (rule.action, rule.duration)) if self._ischeckAdvanceded: rule_temp_name = "%s-list" % rule_temp_name else: rule_temp_name = "%s-simple" % rule_temp_name rule_temp_name = slugify("%s %s" % (rule_temp_name, rule.operator.data)) return rule_temp_name[:128] def _send_rule(self): self._cfg.setSettings("promptDialog/geometry", self.saveGeometry()) self._rule = ui_pb2.Rule(name="user.choice") self._rule.enabled = True self._rule.action = Config.ACTION_DENY if self._default_action == self.ACTION_IDX_DENY else Config.ACTION_ALLOW self._rule.duration = self._get_duration(self.durationCombo.currentIndex()) what_idx = self.whatCombo.currentIndex() self._rule.operator.type, self._rule.operator.operand, self._rule.operator.data = self._get_combo_operator(self.whatCombo, what_idx) if self._rule.operator.data == "": print("Invalid rule, discarding: ", self._rule) self._rule = None self._done.set() return rule_temp_name = self._get_rule_name(self._rule) self._rule.name = rule_temp_name # TODO: move to a method data=[] if self._ischeckAdvanceded and self.checkDstIP.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_DST_IP: _type, _operand, _data = self._get_combo_operator(self.whatIPCombo, self.whatIPCombo.currentIndex()) data.append({"type": _type, "operand": _operand, "data": _data}) rule_temp_name = slugify("%s %s" % (rule_temp_name, _data)) if self._ischeckAdvanceded and self.checkDstPort.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_DST_PORT: data.append({"type": "simple", "operand": "dest.port", "data": str(self._con.dst_port)}) rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.dst_port))) if self._ischeckAdvanceded and self.checkUserID.isChecked() and self.whatCombo.itemData(what_idx) != self.FIELD_USER_ID: data.append({"type": "simple", "operand": "user.id", "data": str(self._con.user_id)}) rule_temp_name = slugify("%s %s" % (rule_temp_name, str(self._con.user_id))) if self._ischeckAdvanceded: data.append({"type": self._rule.operator.type, "operand": self._rule.operator.operand, "data": self._rule.operator.data}) self._rule.operator.data = json.dumps(data) self._rule.operator.type = "list" self._rule.operator.operand = "" self._rule.name = rule_temp_name self.hide() if self._ischeckAdvanceded: self.checkAdvanced.toggle() self._ischeckAdvanceded = False # signal that the user took a decision and # a new rule is available self._done.set()
class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _trigger = QtCore.pyqtSignal() def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) self.setupUi(self) self.setWindowTitle("opensnitch-qt v%s" % version) self._lock = threading.Lock() self._con = None self._rule = None self._trigger.connect(self.on_connection_triggered) self._done = threading.Event() self._apps_parser = LinuxDesktopParser() self._app_name_label = self.findChild(QtWidgets.QLabel, "appNameLabel") self._app_icon_label = self.findChild(QtWidgets.QLabel, "iconLabel") self._message_label = self.findChild(QtWidgets.QLabel, "messageLabel") self._src_ip_label = self.findChild(QtWidgets.QLabel, "sourceIPLabel") self._dst_ip_label = self.findChild(QtWidgets.QLabel, "destIPLabel") self._dst_port_label = self.findChild(QtWidgets.QLabel, "destPortLabel") self._dst_host_label = self.findChild(QtWidgets.QLabel, "destHostLabel") self._uid_label = self.findChild(QtWidgets.QLabel, "uidLabel") self._pid_label = self.findChild(QtWidgets.QLabel, "pidLabel") self._args_label = self.findChild(QtWidgets.QLabel, "argsLabel") self._apply_button = self.findChild(QtWidgets.QPushButton, "applyButton") self._apply_button.clicked.connect(self._on_apply_clicked) self._action_combo = self.findChild(QtWidgets.QComboBox, "actionCombo") self._what_combo = self.findChild(QtWidgets.QComboBox, "whatCombo") self._duration_combo = self.findChild(QtWidgets.QComboBox, "durationCombo") def promptUser(self, connection): # one at a time with self._lock: # reset state self._rule = None self._con = connection self._done.clear() # trigger on_connection_triggered self._trigger.emit() # wait for user choice self._done.wait() return self._rule @QtCore.pyqtSlot() def on_connection_triggered(self): self._render_connection(self._con) self.show() def _render_connection(self, con): app_name, app_icon, desk = self._apps_parser.get_info_by_path( con.process_path, "terminal") if app_name == "": self._app_name_label.setText(con.process_path) else: self._app_name_label.setText(app_name) icon = QtGui.QIcon().fromTheme(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) self._app_icon_label.setPixmap(pixmap) self._message_label.setText("<b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ con.process_path, con.dst_host or con.dst_ip, con.protocol, con.dst_port )) self._src_ip_label.setText(con.src_ip) self._dst_ip_label.setText(con.dst_ip) self._dst_port_label.setText("%s" % con.dst_port) self._dst_host_label.setText(con.dst_host) self._uid_label.setText( "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name)) self._pid_label.setText("%s" % con.process_id) self._args_label.setText(' '.join(con.process_args)) self._what_combo.clear() self._what_combo.addItem("from this process") self._what_combo.addItem("from user %d" % con.user_id) self._what_combo.addItem("to port %d" % con.dst_port) self._what_combo.addItem("to %s" % con.dst_ip) if con.dst_host != "": self._what_combo.addItem("to %s" % con.dst_host) self._what_combo.setCurrentIndex(0) self._action_combo.setCurrentIndex(0) self._duration_combo.setCurrentIndex(1) self.setFixedSize(self.size()) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): if not event.key() == QtCore.Qt.Key_Escape: super(Dialog, self).keyPressEvent(event) # prevent a click on the window's x # from quitting the whole application def closeEvent(self, e): self._on_apply_clicked() e.ignore() def _on_apply_clicked(self): self._rule = ui_pb2.RuleReply(name="user.choice") action_idx = self._action_combo.currentIndex() if action_idx == 0: self._rule.action = "allow" else: self._rule.action = "deny" duration_idx = self._duration_combo.currentIndex() if duration_idx == 0: self._rule.duration = "once" elif duration_idx == 1: self._rule.duration = "until restart" else: self._rule.duration = "always" what_idx = self._what_combo.currentIndex() if what_idx == 0: self._rule.what = "process.path" self._rule.value = self._con.process_path elif what_idx == 1: self._rule.what = "user.id" self._rule.value = "%s" % self._con.user_id elif what_idx == 2: self._rule.what = "dest.port" self._rule.value = "%s" % self._con.dst_port elif what_idx == 3: self._rule.what = "dest.ip" self._rule.value = self._con.dst_ip else: self._rule.what = "dest.host" self._rule.value = self._con.dst_host self._rule.name = slugify( "%s %s %s" % (self._rule.action, self._rule.what, self._rule.value)) self.hide() # signal that the user took a decision and # a new rule is available self._done.set()
class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]): _prompt_trigger = QtCore.pyqtSignal() _tick_trigger = QtCore.pyqtSignal() _timeout_trigger = QtCore.pyqtSignal() def __init__(self, parent=None): QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint) self._cfg = Config.get() self._db = Database.instance() dialog_geometry = self._cfg.getSettings("promptDialog/geometry") if dialog_geometry != None: self.restoreGeometry(dialog_geometry) self.setupUi(self) self.setWindowTitle("OpenSnitch v%s" % version) self._lock = threading.Lock() self._con = None self._rule = None self._local = True self._peer = None self._prompt_trigger.connect(self.on_connection_prompt_triggered) self._timeout_trigger.connect(self.on_timeout_triggered) self._tick_trigger.connect(self.on_tick_triggered) self._tick = self._cfg.default_timeout self._tick_thread = None self._done = threading.Event() self._apply_text = "Apply" self._apps_parser = LinuxDesktopParser() self._app_name_label = self.findChild(QtWidgets.QLabel, "appNameLabel") self._app_icon_label = self.findChild(QtWidgets.QLabel, "iconLabel") self._message_label = self.findChild(QtWidgets.QLabel, "messageLabel") self._src_ip_label = self.findChild(QtWidgets.QLabel, "sourceIPLabel") self._dst_ip_label = self.findChild(QtWidgets.QLabel, "destIPLabel") self._dst_port_label = self.findChild(QtWidgets.QLabel, "destPortLabel") self._uid_label = self.findChild(QtWidgets.QLabel, "uidLabel") self._pid_label = self.findChild(QtWidgets.QLabel, "pidLabel") self._args_label = self.findChild(QtWidgets.QLabel, "argsLabel") self._apply_button = self.findChild(QtWidgets.QPushButton, "applyButton") self._apply_button.clicked.connect(self._on_apply_clicked) self._action_combo = self.findChild(QtWidgets.QComboBox, "actionCombo") self._what_combo = self.findChild(QtWidgets.QComboBox, "whatCombo") self._what_dstip_combo = self.findChild(QtWidgets.QComboBox, "whatIPCombo") self._duration_combo = self.findChild(QtWidgets.QComboBox, "durationCombo") self._what_dstip_combo.setVisible(False) self._dst_ip_check = self.findChild(QtWidgets.QCheckBox, "checkDstIP") self._dst_port_check = self.findChild(QtWidgets.QCheckBox, "checkDstPort") self._uid_check = self.findChild(QtWidgets.QCheckBox, "checkUserID") self._advanced_check = self.findChild(QtWidgets.QPushButton, "checkAdvanced") self._dst_ip_check.setVisible(False) self._dst_port_check.setVisible(False) self._uid_check.setVisible(False) self._is_advanced_checked = False self._advanced_check.toggled.connect(self._checkbox_toggled) def _checkbox_toggled(self, state): self._apply_button.setText("%s" % self._apply_text) self._tick_thread.stop = state self._dst_ip_check.setVisible(state) self._what_dstip_combo.setVisible(state) self._dst_ip_label.setVisible(not state) self._dst_port_check.setVisible(state) self._uid_check.setVisible(state) self._is_advanced_checked = state def promptUser(self, connection, is_local, peer): # one at a time with self._lock: # reset state if self._tick_thread != None and self._tick_thread.is_alive(): self._tick_thread.join() self._tick = self._cfg.default_timeout self._tick_thread = threading.Thread(target=self._timeout_worker) self._tick_thread.stop = self._is_advanced_checked self._rule = None self._local = is_local self._peer = peer self._con = connection self._done.clear() # trigger and show dialog self._prompt_trigger.emit() # start timeout thread self._tick_thread.start() # wait for user choice or timeout self._done.wait() return self._rule def _timeout_worker(self): while self._tick > 0 and self._done.is_set() is False: t = threading.currentThread() if getattr(t, "stop", True): self._tick = self._cfg.default_timeout continue self._tick -= 1 self._tick_trigger.emit() time.sleep(1) if not self._done.is_set(): self._timeout_trigger.emit() @QtCore.pyqtSlot() def on_connection_prompt_triggered(self): self._render_connection(self._con) self.show() @QtCore.pyqtSlot() def on_tick_triggered(self): self._apply_button.setText("%s (%d)" % (self._apply_text, self._tick)) @QtCore.pyqtSlot() def on_timeout_triggered(self): self._on_apply_clicked() def _render_connection(self, con): if self._local: app_name, app_icon, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal") else: app_name, app_icon = "", "terminal" if app_name == "": app_name = "Unknown process" self._app_name_label.setText("Outgoing connection") else: self._app_name_label.setText(app_name) icon = QtGui.QIcon().fromTheme(app_icon) pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48))) self._app_icon_label.setPixmap(pixmap) if self._local: message = "<b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, con.dst_host or con.dst_ip, con.protocol, con.dst_port ) else: message = "The process <b>%s</b> running on the computer <b>%s</b> is connecting to <b>%s</b> on %s port %d" % ( \ app_name, self._peer.split(':')[1], con.dst_host or con.dst_ip, con.protocol, con.dst_port ) self._message_label.setText(message) self._src_ip_label.setText(con.src_ip) self._dst_ip_label.setText(con.dst_ip) self._dst_port_label.setText(str(con.dst_port)) if self._local: try: uid = "%d (%s)" % (con.user_id, pwd.getpwuid(con.user_id).pw_name) except: uid = "" else: uid = "%d" % con.user_id self._uid_label.setText(uid) self._pid_label.setText("%s" % con.process_id) self._args_label.setText(' '.join(con.process_args)) self._what_combo.clear() self._what_dstip_combo.clear() if int(con.process_id) > 0: self._what_combo.addItem("from this process", "process_id") if int(con.user_id) >= 0: self._what_combo.addItem("from user %s" % uid, "user_id") self._what_combo.addItem("to port %d" % con.dst_port, "dst_port") self._what_combo.addItem("to %s" % con.dst_ip, "dst_ip") if con.dst_host != "" and con.dst_host != con.dst_ip: self._what_combo.addItem("to %s" % con.dst_host, "simple_host") self._what_dstip_combo.addItem("to %s" % con.dst_host, "simple_host") parts = con.dst_host.split('.')[1:] nparts = len(parts) for i in range(0, nparts - 1): self._what_combo.addItem("to *.%s" % '.'.join(parts[i:]), "regex_host") self._what_dstip_combo.addItem("to *.%s" % '.'.join(parts[i:]), "regex_host") self._what_dstip_combo.addItem("to %s" % con.dst_ip, "dst_ip") parts = con.dst_ip.split('.') nparts = len(parts) for i in range(1, nparts): self._what_combo.addItem("to %s.*" % '.'.join(parts[:i]), "regex_ip") self._what_dstip_combo.addItem("to %s.*" % '.'.join(parts[:i]), "regex_ip") if self._cfg.default_action == "allow": self._action_combo.setCurrentIndex(0) else: self._action_combo.setCurrentIndex(1) if self._cfg.default_duration == "once": self._duration_combo.setCurrentIndex(0) elif self._cfg.default_duration == "30s": self._duration_combo.setCurrentIndex(1) elif self._cfg.default_duration == "5m": self._duration_combo.setCurrentIndex(2) elif self._cfg.default_duration == "15m": self._duration_combo.setCurrentIndex(3) elif self._cfg.default_duration == "30m": self._duration_combo.setCurrentIndex(4) elif self._cfg.default_duration == "1h": self._duration_combo.setCurrentIndex(5) elif self._cfg.default_duration == "until restart": self._duration_combo.setCurrentIndex(6) else: self._duration_combo.setCurrentIndex(7) if int(con.process_id) > 0: self._what_combo.setCurrentIndex(0) else: self._what_combo.setCurrentIndex(1) self._apply_button.setText("Apply (%d)" % self._tick) self.setFixedSize(self.size()) # https://gis.stackexchange.com/questions/86398/how-to-disable-the-escape-key-for-a-dialog def keyPressEvent(self, event): if not event.key() == QtCore.Qt.Key_Escape: super(PromptDialog, self).keyPressEvent(event) # prevent a click on the window's x # from quitting the whole application def closeEvent(self, e): self._on_apply_clicked() e.ignore() def _get_duration(self, duration_idx): if duration_idx == 0: return "once" elif duration_idx == 1: return "30s" elif duration_idx == 2: return "5m" elif duration_idx == 3: return "15m" elif duration_idx == 4: return "30m" elif duration_idx == 5: return "1h" elif duration_idx == 6: return "until restart" else: return "always" def _get_combo_operator(self, combo, what_idx): if combo.itemData(what_idx) == "process_id": return "simple", "process.path", self._con.process_path elif combo.itemData(what_idx) == "user_id": return "simple", "user.id", "%s" % self._con.user_id elif combo.itemData(what_idx) == "dst_port": return "simple", "dest.port", "%s" % self._con.dst_port elif combo.itemData(what_idx) == "dst_ip": return "simple", "dest.ip", self._con.dst_ip elif combo.itemData(what_idx) == "simple_host": return "simple", "dest.host", self._con.dst_host elif combo.itemData(what_idx) == "regex_host": return "regexp", "dest.host", "%s" % '\.'.join(combo.currentText().split('.')).replace("*", ".*")[3:] elif combo.itemData(what_idx) == "regex_ip": return "regexp", "dest.ip", "%s" % '\.'.join(combo.currentText().split('.')).replace("*", ".*")[3:] def _on_apply_clicked(self): self._cfg.setSettings("promptDialog/geometry", self.saveGeometry()) self._rule = ui_pb2.Rule(name="user.choice") action_idx = self._action_combo.currentIndex() if action_idx == 0: self._rule.action = "allow" else: self._rule.action = "deny" self._rule.duration = self._get_duration(self._duration_combo.currentIndex()) what_idx = self._what_combo.currentIndex() self._rule.operator.type, self._rule.operator.operand, self._rule.operator.data = self._get_combo_operator(self._what_combo, what_idx) # TODO: move to a method is_advanced=False data=[] if self._dst_ip_check.isChecked() and (self._what_combo.itemData(what_idx) == "process_id" or self._what_combo.itemData(what_idx) == "user_id" or self._what_combo.itemData(what_idx) == "dst_port"): is_advanced=True _type, _operand, _data = self._get_combo_operator(self._what_dstip_combo, self._what_dstip_combo.currentIndex()) data.append({"type": _type, "operand": _operand, "data": _data}) if self._dst_port_check.isChecked() and self._what_combo.itemData(what_idx) != "dst_port": is_advanced=True data.append({"type": "simple", "operand": "dest.port", "data": str(self._con.dst_port)}) if self._uid_check.isChecked() and self._what_combo.itemData(what_idx) != "user_id": is_advanced=True data.append({"type": "simple", "operand": "user.id", "data": str(self._con.user_id)}) if is_advanced and self._advanced_check.isChecked(): data.append({"type": self._rule.operator.type, "operand": self._rule.operator.operand, "data": self._rule.operator.data}) self._rule.operator.data = json.dumps(data) self._rule.operator.type = "list" self._rule.operator.operand = "" self._rule.name = slugify("%s %s %s" % (self._rule.action, self._rule.operator.type, self._rule.operator.data)) self.hide() if self._is_advanced_checked: self._advanced_check.toggle() self._id_advanced_checked = False # signal that the user took a decision and # a new rule is available self._done.set()