ui, pop-ups: added app description, better icon discovery

Added the description of an app to the pop-ups, to help users know
what an application is or does.

The discovery of app icons has been improved for those edge cases where
the system is not properly configured and we were not able to get the
icon of the app.
This commit is contained in:
Gustavo Iñiguez Goia 2021-03-03 23:57:25 +01:00
parent c7d93d83a5
commit 0362a0b780
3 changed files with 177 additions and 67 deletions

View file

@ -6,6 +6,7 @@ import glob
import os
import re
import shutil
import locale
DESKTOP_PATHS = tuple([
os.path.join(d, 'applications')
@ -20,6 +21,7 @@ class LinuxDesktopParser(threading.Thread):
self.running = False
self.apps = {}
self.apps_by_name = {}
self.get_locale()
# some things are just weird
# (not really, i don't want to keep track of parent pids
# just because of icons though, this hack is way easier)
@ -37,6 +39,14 @@ class LinuxDesktopParser(threading.Thread):
self.start()
def get_locale(self):
try:
self.locale = locale.getlocale()[0]
self.locale_country = self.locale.split("_")[0]
except Exception as e:
self.locale = ""
self.locale_country = ""
def _parse_exec(self, cmd):
# remove stuff like %U
cmd = re.sub( r'%[a-zA-Z]+', '', cmd)
@ -53,23 +63,39 @@ class LinuxDesktopParser(threading.Thread):
if os.path.exists(filename):
cmd = filename
break
return cmd
def _discover_app_icon(self, app_name):
def get_app_description(self, parser):
try:
desc = parser.get('Desktop Entry', 'Comment[%s]' % self.locale_country, raw=True, fallback=None)
if desc == None:
desc = parser.get('Desktop Entry', 'Comment[%s]' % self.locale, raw=True, fallback=None)
if desc == None:
desc = parser.get('Desktop Entry', 'Comment', raw=True, fallback=None)
return desc
except:
return None
def discover_app_icon(self, app_name):
# more hacks
# normally qt will find icons if the system if configured properly.
# if it's not, qt won't be able to find the icon by using QIcon().fromTheme(""),
# so we fallback to try to determine if the icon exist in some well known system paths.
icon_dirs = ("/usr/share/icons/gnome/48x48/apps/", "/usr/share/pixmaps/", "/usr/share/icons/hicolor/48x48/apps/")
icon_exts = (".png", ".xpm", ".svg")
for idir in icon_dirs:
for iext in icon_exts:
iconPath = idir + app_name + iext
if os.path.exists(iconPath):
print("found on last chance: ", iconPath)
return iconPath
if iext in app_name:
iconPath = idir + app_name
if os.path.exists(iconPath):
return iconPath
else:
iconPath = idir + app_name + iext
if os.path.exists(iconPath):
return iconPath
def _parse_desktop_file(self, desktop_path):
parser = configparser.ConfigParser(strict=False) # Allow duplicate config entries
@ -84,21 +110,23 @@ class LinuxDesktopParser(threading.Thread):
cmd = self._parse_exec(cmd)
icon = parser.get('Desktop Entry', 'Icon', raw=True, fallback=None)
name = parser.get('Desktop Entry', 'Name', raw=True, fallback=None)
desc = self.get_app_description(parser)
if icon == None:
# Some .desktop files doesn't have the Icon entry
# FIXME: even if we return an icon, if the DE is not properly configured,
# it won't be loaded/displayed.
icon = self._discover_app_icon(basename)
icon = self.discover_app_icon(basename)
with self.lock:
# The Exec entry may have an absolute path to a binary or just the binary with parameters.
# /path/binary or binary, so save both
self.apps[cmd] = (name, icon, desktop_path)
self.apps[basename] = (name, icon, desktop_path)
self.apps[cmd] = (name, icon, desc, desktop_path)
self.apps[basename] = (name, icon, desc, desktop_path)
# if the command is a symlink, add the real binary too
if os.path.islink(cmd):
link_to = os.path.realpath(cmd)
self.apps[link_to] = (name, icon, desktop_path)
self.apps[link_to] = (name, icon, desc, desktop_path)
except:
print("Exception parsing .desktop file ", desktop_path)
@ -112,9 +140,9 @@ class LinuxDesktopParser(threading.Thread):
app_name = self.apps.get(path)
if app_name == None:
return self.apps.get(def_name, (def_name, default_icon, None))
return self.apps.get(def_name, (def_name, default_icon, "", None))
return self.apps.get(path, (def_name, default_icon, None))
return self.apps.get(path, (def_name, default_icon, "", None))
def get_info_by_binname(self, name, default_icon):
def_name = os.path.basename(name)

View file

@ -2,11 +2,13 @@ import threading
import sys
import time
import os
import os.path
import pwd
import json
import ipaddress
from PyQt5 import QtCore, QtGui, uic, QtWidgets
from PyQt5.QtCore import QCoreApplication as QC
from slugify import slugify
@ -46,9 +48,9 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
# don't translate
# label displayed in the pop-up combo
DURATION_session = QtCore.QCoreApplication.translate("popups", "until reboot")
DURATION_session = QC.translate("popups", "until reboot")
# label displayed in the pop-up combo
DURATION_forever = QtCore.QCoreApplication.translate("popups", "forever")
DURATION_forever = QC.translate("popups", "forever")
def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowStaysOnTopHint)
@ -81,14 +83,15 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.denyButton.clicked.connect(self._on_deny_clicked)
# also accept button
self.applyButton.clicked.connect(self._on_apply_clicked)
self._apply_text = QtCore.QCoreApplication.translate("popups", "Allow")
self._deny_text = QtCore.QCoreApplication.translate("popups", "Deny")
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)
@ -195,44 +198,61 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.denyButton.setText("%s (%d)" % (self._deny_text, self._tick))
self.applyButton.setText(self._apply_text)
def _render_connection(self, con):
app_name, app_icon, _ = self._apps_parser.get_info_by_path(con.process_path, "terminal")
app_args = " ".join(con.process_args)
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)
self._set_elide_text(self.appPathLabel, "(%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("")
self.argsLabel.setVisible(True)
self._set_elide_text(self.argsLabel, app_args)
self.argsLabel.setToolTip(app_args)
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:
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 = QtCore.QCoreApplication.translate("popups", "Unknown process: %s" % con.process_path)
self.appNameLabel.setText(QtCore.QCoreApplication.translate("popups", "Outgoing connection"))
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" % (QtCore.QCoreApplication.translate("popups", "Process launched from:"), con.process_cwd))
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)
icon = QtGui.QIcon().fromTheme(app_icon)
pixmap = icon.pixmap(icon.actualSize(QtCore.QSize(48, 48)))
pixmap = self._get_app_icon(app_icon)
self.iconLabel.setPixmap(pixmap)
message = self._get_popup_message(app_name, con)
@ -258,29 +278,29 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.whatCombo.clear()
self.whatIPCombo.clear()
if int(con.process_id) > 0:
self.whatCombo.addItem(QtCore.QCoreApplication.translate("popups", "from this executable"), self.FIELD_PROC_PATH)
self.whatCombo.addItem(QC.translate("popups", "from this executable"), self.FIELD_PROC_PATH)
self.whatCombo.addItem(QtCore.QCoreApplication.translate("popups", "from this command line"), self.FIELD_PROC_ARGS)
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(QtCore.QCoreApplication.translate("popups", "to port {0}").format(con.dst_port), self.FIELD_DST_PORT)
self.whatCombo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}").format(con.dst_ip), self.FIELD_DST_IP)
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(QtCore.QCoreApplication.translate("popups", "from user {0}").format(uid), self.FIELD_USER_ID)
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(QtCore.QCoreApplication.translate("popups", "to {0}").format(con.dst_ip), self.FIELD_DST_IP)
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(QtCore.QCoreApplication.translate("popups", "to {0}.*").format('.'.join(parts[:i])), self.FIELD_REGEX_IP)
self.whatIPCombo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}.*").format( '.'.join(parts[:i])), self.FIELD_REGEX_IP)
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)
@ -311,12 +331,12 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
def _add_dst_networks_to_combo(self, combo, dst_ip):
if type(ipaddress.ip_address(dst_ip)) == ipaddress.IPv4Address:
combo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/24", strict=False)), self.FIELD_DST_NETWORK)
combo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/16", strict=False)), self.FIELD_DST_NETWORK)
combo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/8", strict=False)), self.FIELD_DST_NETWORK)
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(QtCore.QCoreApplication.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/64", strict=False)), self.FIELD_DST_NETWORK)
combo.addItem(QtCore.QCoreApplication.translate("popups", "to {0}").format(ipaddress.ip_network(dst_ip + "/128", strict=False)), self.FIELD_DST_NETWORK)
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)
@ -325,12 +345,32 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
parts = dst_host.split('.')[1:]
nparts = len(parts)
for i in range(0, nparts - 1):
self.whatCombo.addItem(QtCore.QCoreApplication.translate("popups", "to *.{0}").format('.'.join(parts[i:])), self.FIELD_REGEX_HOST)
self.whatIPCombo.addItem(QtCore.QCoreApplication.translate("popups", "to *.{0}").format('.'.join(parts[i:])), self.FIELD_REGEX_HOST)
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(QtCore.QCoreApplication.translate("popups", "to *{0}").format(dst_host), self.FIELD_REGEX_HOST)
self.whatIPCombo.addItem(QtCore.QCoreApplication.translate("popups", "to *{0}").format(dst_host), self.FIELD_REGEX_HOST)
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):
"""
@ -340,17 +380,17 @@ class PromptDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
"""
message = "<b>%s</b>" % app_name
if not self._local:
message = QtCore.QCoreApplication.translate("popups", "<b>Remote</b> process %s running on <b>%s</b>") % ( \
message = QC.translate("popups", "<b>Remote</b> process %s running on <b>%s</b>") % ( \
message,
self._peer.split(':')[1])
msg_action = QtCore.QCoreApplication.translate("popups", "is connecting to <b>%s</b> on %s port %d") % ( \
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 = QtCore.QCoreApplication.translate("popups", "is attempting to resolve <b>%s</b> via %s, %s port %d") % ( \
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(),

View file

@ -494,7 +494,7 @@
</property>
<property name="font">
<font>
<pointsize>18</pointsize>
<pointsize>16</pointsize>
<weight>75</weight>
<bold>true</bold>
<kerning>true</kerning>
@ -511,14 +511,8 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="argsLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item row="1" column="1">
<widget class="QLabel" name="appDescriptionLabel">
<property name="maximumSize">
<size>
<width>500</width>
@ -528,24 +522,22 @@
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>50</weight>
<italic>true</italic>
<bold>false</bold>
<underline>true</underline>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;/opt/google/chrome/bin/chrome --something abc --more-long def --for-word-wrapping&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item row="1" column="1">
<item row="3" column="1">
<widget class="QLabel" name="appPathLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
@ -573,6 +565,56 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="argsLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>500</width>
<height>60</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string>(/path/to/bin/chromium)</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>