Skip to content

Commit

Permalink
Handle HTTP & HTTPS URLs in dom0 & GUIVMs
Browse files Browse the repository at this point in the history
A new `qubes-virtual-browser` tool is made to handle URLs within dom0
and/or GUIVMs. It could safely open URLs via DisposableVMs, copy them to
global clipboard or discard them.

resolves: QubesOS/qubes-issues#8171
  • Loading branch information
alimirjamali committed Nov 10, 2024
1 parent bab3289 commit dd980e9
Show file tree
Hide file tree
Showing 14 changed files with 785 additions and 43 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,5 @@ ENV/
*.swp

*.~undo-tree~
*.glade~
*.glade#
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ checks:tests:
before_script: &before-script
- "PATH=$PATH:$HOME/.local/bin"
- sudo dnf install -y python3-gobject gtk3 python3-pytest gtksourceview4
python3-coverage xorg-x11-server-Xvfb python3-pip
python3-coverage xorg-x11-server-Xvfb python3-pip xdg-utils
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
- git clone https://github.com/QubesOS/qubes-core-qrexec ~/core-qrexec
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ install-autostart:
cp desktop/qubes-global-config.desktop $(DESTDIR)/usr/share/applications/
cp desktop/qubes-new-qube.desktop $(DESTDIR)/usr/share/applications/
cp desktop/qubes-policy-editor-gui.desktop $(DESTDIR)/usr/share/applications/
cp desktop/qubes-virtual-browser.desktop $(DESTDIR)/usr/share/applications/

install-lang:
mkdir -p $(DESTDIR)/usr/share/gtksourceview-4/language-specs/
Expand Down
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Package: qubes-desktop-linux-manager
Architecture: any
Depends:
python3-qui,
xdg-utils,
${misc:Depends}
Description: Qubes UI Applications
A collection of GUI application for enhancing the Qubes UX.
Expand Down
13 changes: 13 additions & 0 deletions desktop/qubes-virtual-browser.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### Note: With this installed, typing "xdg-settings set default-web-browser qubes-virtual-browser.desktop" will make it so that in gnome-terminal
### (typing "xdg-settings set default-web-browser firefox.desktop" will put it back to normal)

[Desktop Entry]
Version=1.0
Name=Qubes Virtual Browser
Exec=/usr/bin/qubes-virtual-browser %u
Icon=qubes-manager
Terminal=false
Type=Application
Categories=Network;WebBrowser;
MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;
NoDisplay=true
227 changes: 215 additions & 12 deletions qubes_config/global_config.glade

Large diffs are not rendered by default.

157 changes: 144 additions & 13 deletions qubes_config/global_config/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import qubesadmin.vm
from ..widgets.gtk_utils import show_error, show_dialog_with_icon, load_theme
from ..widgets.gtk_widgets import ProgressBarDialog, ViewportHandler
from ..widgets.utils import open_url_in_disposable
from ..widgets.utils import open_url_in_disposable, apply_feature_change
from .page_handler import PageHandler
from .policy_handler import PolicyHandler, VMSubsetPolicyHandler
from .policy_rules import RuleSimple, \
Expand All @@ -49,6 +49,7 @@

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, GObject
from gi.repository.GdkPixbuf import Pixbuf

logger = logging.getLogger('qubes-global-config')

Expand Down Expand Up @@ -180,6 +181,138 @@ def get_unsaved(self) -> str:
return "\n".join(unsaved)


class UrlPageHandler(PageHandler):
"""Handler for URL page. Requires separate handler because it combines
qubes-virtual-browser settings with qubes.OpenURL policies. """
def __init__(self, qapp: qubesadmin.Qubes,
gtk_builder: Gtk.Builder,
policy_manager: PolicyManager):
self.qapp = qapp
self.builder = gtk_builder
self.policy_manager = policy_manager
self.browser_action = qapp.domains[qapp.local_name].features.get( \
"virtual-browser-action", "")

self.virtual_browser_ask: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_ask')
self.virtual_browser_dispvm: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_dispvm')
self.virtual_browser_clipboard: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_clipboard')
self.virtual_browser_discard: Gtk.RadioButton = \
gtk_builder.get_object('virtual_browser_discard')

self.virtual_browser_dispvms: Gtk.ComboBox = \
gtk_builder.get_object('virtual_browser_dispvms')
self.disposables = Gtk.ListStore(object, Pixbuf, str)
self.virtual_browser_dispvms.set_model(self.disposables)
self.renderer_icon = Gtk.CellRendererPixbuf()
self.renderer_vmname = Gtk.CellRendererText()
self.virtual_browser_dispvms.pack_start(self.renderer_icon, True)
self.virtual_browser_dispvms.pack_start(self.renderer_vmname, True)
self.virtual_browser_dispvms.add_attribute( \
self.renderer_icon, "pixbuf", 1)
self.virtual_browser_dispvms.add_attribute( \
self.renderer_vmname, "text", 2)

default_dispvm = getattr(self.qapp, "default_dispvm", None)
for domain in qapp.domains:
if getattr(domain, "template_for_dispvms", False):
# pylint: disable=no-member
try:
icon = Gtk.IconTheme.get_default().load_icon(
getattr(domain, "icon", "qubes-manager"), 32, 0)
except gi.repository.GLib.GError:

Check warning on line 225 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L225

Added line #L225 was not covered by tests
# Use Adwaita's Qube like icon if original icon is missing
icon = Gtk.IconTheme.get_default().load_icon(

Check warning on line 227 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L227

Added line #L227 was not covered by tests
"insert-object-symbolic", 32, 0)
row = self.disposables.append([domain, icon, domain.name])
if domain.name == default_dispvm:
self.disposables[row][2] += " (Default DispVM Template)"

Check warning on line 231 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L231

Added line #L231 was not covered by tests

self._reset_virtual_browser_choice()
self.virtual_browser_dispvms.connect("changed", self._dispvm_selected)

# Allocating a list of handlers in case we want to add more in future
self.handlers: List[Union[DispvmExceptionHandler, FeatureHandler]] = [
DispvmExceptionHandler(
gtk_builder=self.builder,
qapp=self.qapp,
service_name="qubes.OpenURL",
policy_file_name="50-config-openurl",
prefix="url",
policy_manager=self.policy_manager,
)
]

def _dispvm_selected(self, combo):
# pylint: disable=unused-argument
self.virtual_browser_dispvm.set_active(True)

Check warning on line 250 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L250

Added line #L250 was not covered by tests

def _reset_virtual_browser_choice(self):
if self.browser_action is not None \
and self.browser_action.startswith('disposable:') \
and self.browser_action[11:] in self.qapp.domains:
self.virtual_browser_dispvm.set_active(True)

Check warning on line 256 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L256

Added line #L256 was not covered by tests
elif self.browser_action == 'clipboard':
self.virtual_browser_clipboard.set_active(True)

Check warning on line 258 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L258

Added line #L258 was not covered by tests
elif self.browser_action == 'discard':
self.virtual_browser_discard.set_active(True)

Check warning on line 260 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L260

Added line #L260 was not covered by tests
else:
self.virtual_browser_ask.set_active(True)

default_dispvm = getattr(self.qapp, "default_dispvm", None)
for index, disposable in enumerate(self.disposables):
if self.virtual_browser_dispvm.get_active():
if disposable[0].name == self.browser_action[11:]:
self.virtual_browser_dispvms.set_active(index)

Check warning on line 268 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L267-L268

Added lines #L267 - L268 were not covered by tests
elif disposable[0].name == default_dispvm:
self.virtual_browser_dispvms.set_active(index)

Check warning on line 270 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L270

Added line #L270 was not covered by tests

def reset(self):
for handler in self.handlers:
handler.reset()
self._reset_virtual_browser_choice()

Check warning on line 275 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L273-L275

Added lines #L273 - L275 were not covered by tests

def save(self):
for handler in self.handlers:
handler.save()
if self.virtual_browser_ask.get_active():
self.browser_action = None
elif self.virtual_browser_dispvm.get_active():
tree_iter = self.virtual_browser_dispvms.get_active_iter()
self.browser_action = "disposable:" + \

Check warning on line 284 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L278-L284

Added lines #L278 - L284 were not covered by tests
self.disposables[tree_iter][0].name
elif self.virtual_browser_clipboard.get_active():
self.browser_action = "clipboard"
elif self.virtual_browser_discard.get_active():
self.browser_action = "discard"
apply_feature_change(self.qapp.domains[self.qapp.local_name], \

Check warning on line 290 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L286-L290

Added lines #L286 - L290 were not covered by tests
"virtual-browser-action", self.browser_action)

def _virtual_browser_choice(self) -> str:
if self.virtual_browser_ask.get_active():
return ""
if self.virtual_browser_dispvm.get_active():
tree_iter = self.virtual_browser_dispvms.get_active_iter()
return "disposable:" + self.disposables[tree_iter][0].name
if self.virtual_browser_clipboard.get_active():
return "clipboard"
if self.virtual_browser_discard.get_active():
return "discard"
return ""

Check warning on line 303 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L296-L303

Added lines #L296 - L303 were not covered by tests

def get_unsaved(self) -> str:
unsaved = []
for handler in self.handlers:
unsaved_changes = handler.get_unsaved()
if unsaved_changes:
unsaved.append(unsaved_changes)

Check warning on line 310 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L310

Added line #L310 was not covered by tests
if self.browser_action != self._virtual_browser_choice():
unsaved.append(_("Qubes Virtual Browser default action"))

Check warning on line 312 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L312

Added line #L312 was not covered by tests
return "\n".join(unsaved)


class GlobalConfig(Gtk.Application):
"""
Main Gtk.Application for new qube widget.
Expand Down Expand Up @@ -327,14 +460,12 @@ def perform_setup(self):
policy_manager=self.policy_manager
)
self.progress_bar_dialog.update_progress(page_progress)
self.handlers['url'] = DispvmExceptionHandler(
gtk_builder=self.builder,
qapp=self.qapp,
service_name="qubes.OpenURL",
policy_file_name="50-config-openurl",
prefix="url",
policy_manager=self.policy_manager,
)

self.handlers['url'] = UrlPageHandler(
qapp=self.qapp,
gtk_builder=self.builder,
policy_manager=self.policy_manager
)
self.progress_bar_dialog.update_progress(page_progress)

self.handlers['thisdevice'] = ThisDeviceHandler(self.qapp,
Expand Down Expand Up @@ -376,7 +507,7 @@ def _handle_urls(self):
label.connect("activate-link", self._activate_link)

def _activate_link(self, _widget, url):
open_url_in_disposable(url, self.qapp)
open_url_in_disposable(url)

Check warning on line 510 in qubes_config/global_config/global_config.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/global_config/global_config.py#L510

Added line #L510 was not covered by tests
return True

def get_current_page(self) -> Optional[PageHandler]:
Expand Down Expand Up @@ -444,9 +575,9 @@ def _ask_unsaved(self, description: str) -> Gtk.ResponseType:
label_3 = Gtk.Label()
label_3.set_text(_("Do you want to save changes?"))
label_3.set_xalign(0)
box.pack_start(label_1, False, False, 10)
box.pack_start(label_2, False, False, 10)
box.pack_start(label_3, False, False, 10)
box.pack_start(label_1, False, False, 10) # pylint: disable=no-member
box.pack_start(label_2, False, False, 10) # pylint: disable=no-member
box.pack_start(label_3, False, False, 10) # pylint: disable=no-member

response = show_dialog_with_icon(
parent=self.main_window, title=_("Unsaved changes"), text=box,
Expand Down
4 changes: 1 addition & 3 deletions qubes_config/policy_editor/policy_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import gi
import importlib.resources

import qubesadmin
from qrexec.policy.admin_client import PolicyClient
from qrexec.policy.parser import StringPolicy
from qrexec.exc import PolicySyntaxError
Expand Down Expand Up @@ -246,8 +245,7 @@ def perform_setup(self):

@staticmethod
def _open_docs(_widget, url):
qapp = qubesadmin.Qubes()
open_url_in_disposable(url, qapp)
open_url_in_disposable(url)

Check warning on line 248 in qubes_config/policy_editor/policy_editor.py

View check run for this annotation

Codecov / codecov/patch

qubes_config/policy_editor/policy_editor.py#L248

Added line #L248 was not covered by tests
return True

def setup_actions(self):
Expand Down
9 changes: 6 additions & 3 deletions qubes_config/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ def test_qapp_impl():
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])
add_expected_vm(qapp, 'sys-net', 'AppVM',
{'provides_network': ('bool', False, 'True')},
Expand Down Expand Up @@ -373,7 +374,8 @@ def test_qapp_simple(): # pylint: disable=redefined-outer-name
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])
add_expected_vm(qapp, 'sys-net', 'AppVM',
{'provides_network': ('bool', False, 'True')},
Expand Down Expand Up @@ -427,7 +429,8 @@ def test_qapp_broken(): # pylint: disable=redefined-outer-name
'config.default.qubes-update-check': None,
'config-usbvm-name': None,
'gui-default-secure-copy-sequence': None,
'gui-default-secure-paste-sequence': None
'gui-default-secure-paste-sequence': None,
'virtual-browser-action': None
}, [])

#
Expand Down
12 changes: 5 additions & 7 deletions qubes_config/widgets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,15 @@ def compare_rule_lists(rule_list_1: List[Rule],
return False
return True

def _open_url_in_dvm(url, default_dvm: qubesadmin.vm.QubesVM):
def _open_url(url):
subprocess.run(
['qvm-run', '-p', '--service', f'--dispvm={default_dvm}',
'qubes.OpenURL'], input=url.encode(), check=False,
['qubes-virtual-browser', url.encode()], input=None, check=False,
stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)

def open_url_in_disposable(url: str, qapp: qubesadmin.Qubes):
def open_url_in_disposable(url: str):
"""Open provided url in disposable qube based on default disposable
template"""
default_dvm = qapp.default_dispvm
open_thread = threading.Thread(group=None,
target=_open_url_in_dvm,
args=[url, default_dvm])
target=_open_url,
args=[url])
open_thread.start()
Loading

0 comments on commit dd980e9

Please sign in to comment.