From 3527aab152657925f09be8ff8ef5ea4e3148d7a8 Mon Sep 17 00:00:00 2001 From: jampe Date: Wed, 30 Mar 2022 09:25:49 +0200 Subject: [PATCH 1/4] add multi monitor screenshare support - change excessive width / height / fps runtime error to a warning (multi monitor screenshare often exceeds this check) - add parameter for qubes-video-companion screenshare [screenId] which enables a user to share a specific screen only Signed-off-by: jampe --- receiver/qubes-video-companion | 8 ++++-- receiver/receiver.py | 16 +++++++++--- sender/screenshare.py | 47 ++++++++++++++++++++++++++++++---- sender/service.py | 8 ++++-- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/receiver/qubes-video-companion b/receiver/qubes-video-companion index 95d7a69..89fe52a 100755 --- a/receiver/qubes-video-companion +++ b/receiver/qubes-video-companion @@ -9,7 +9,7 @@ set -E # Enable function inheritance of traps trap exit ERR usage() { - echo "Usage: qubes-video-companion webcam|screenshare" + echo "Usage: qubes-video-companion webcam|screenshare [screenId]" } >&2 video_source="$1" @@ -53,4 +53,8 @@ fi /usr/share/qubes-video-companion/receiver/setup.sh # Disabling buffering should lower latency and it doesn't seem to impact performance # Filter standard error escape characters for safe printing to the terminal from the video sender -qrexec-client-vm --buffer-size=0 --filter-escape-chars-stderr -- dom0 "$qvc_service" /usr/share/qubes-video-companion/receiver/receiver.py +if [ -z "$2" ]; then + qrexec-client-vm --buffer-size=0 --filter-escape-chars-stderr -- dom0 "$qvc_service" /usr/share/qubes-video-companion/receiver/receiver.py +else + qrexec-client-vm --buffer-size=0 --filter-escape-chars-stderr -- dom0 "$qvc_service+$2" /usr/share/qubes-video-companion/receiver/receiver.py +fi diff --git a/receiver/receiver.py b/receiver/receiver.py index 3d49145..95b5fe0 100644 --- a/receiver/receiver.py +++ b/receiver/receiver.py @@ -4,9 +4,14 @@ # Copyright (C) 2021 Demi Marie Obenour # Licensed under the MIT License. See LICENSE file for details. -import sys -import struct import os +import struct +import sys + +import gi +gi.require_version('Gdk', '3.0') +from gi.repository import Gdk + from typing import NoReturn def main(argv) -> NoReturn: @@ -52,13 +57,16 @@ def read_video_parameters() -> (int, int, int): raise AssertionError('bug') untrusted_input = os.read(0, input_size) + if not untrusted_input: + raise RuntimeError('can not read from stream') if len(untrusted_input) != input_size: raise RuntimeError('wrong number of bytes read') untrusted_width, untrusted_height, untrusted_fps = s.unpack(untrusted_input) del untrusted_input - if untrusted_width > 4096 or untrusted_height > 4096 or untrusted_fps > 4096: - raise RuntimeError('excessive width, height, and/or fps') + screen = Gdk.Display().get_default().get_default_screen() + if untrusted_width > screen.width() or untrusted_height > screen.height() or untrusted_fps > 4096: + print('warning: excessive width, height, and/or fps') width, height, fps = untrusted_width, untrusted_height, untrusted_fps del untrusted_width, untrusted_height, untrusted_fps diff --git a/sender/screenshare.py b/sender/screenshare.py index abd7e07..a34b70e 100644 --- a/sender/screenshare.py +++ b/sender/screenshare.py @@ -13,10 +13,11 @@ gi.require_version('Gdk', '3.0') from gi.repository import Gdk from service import Service +import sys class ScreenShare(Service): """Screen sharing video souce class""" - + _share_specific_screen = None def __init__(self): self.main(self) @@ -26,10 +27,33 @@ def video_source(self) -> str: def icon(self) -> str: return 'video-display' + def get_specific_monitor(self, monitor_id): + amount_of_monitors = Gdk.Display().get_default().get_n_monitors() + if monitor_id >= amount_of_monitors: + return None + + return Gdk.Display().get_default().get_monitor(monitor_id) + + def parse_args(self): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('screen', type=int, nargs='?', default=None, help='id of the screen to share') + + args = parser.parse_args() + self._share_specific_screen = args.screen + def parameters(self): - monitor = Gdk.Display().get_default().get_monitor(0) - scale, geometry = monitor.get_scale_factor(), monitor.get_geometry() - return (scale * geometry.width, scale * geometry.height, 30) + if self._share_specific_screen != None: + monitor = self.get_specific_monitor(self._share_specific_screen) + if not monitor: + raise RuntimeError("requested screen to share does not exist") + + scale, geometry = monitor.get_scale_factor(), monitor.get_geometry() + return (scale * geometry.width, scale * geometry.height, 30) + + else: + screen = Gdk.Display().get_default().get_default_screen() + return (screen.width(), screen.height(), 30) def pipeline(self, width: int, height: int, fps: int): caps = ('width={0},' @@ -39,7 +63,7 @@ def pipeline(self, width: int, height: int, fps: int): 'pixel-aspect-ratio=1/1,' 'max-framerate={2}/1,' 'views=1'.format(width, height, fps)) - return [ + args = [ 'ximagesrc', 'use-damage=false', '!', @@ -55,6 +79,19 @@ def pipeline(self, width: int, height: int, fps: int): '!', 'fdsink', ] + if self._share_specific_screen != None: + monitor = self.get_specific_monitor(self._share_specific_screen) + if not monitor: + raise RuntimeError("requested screen to share does not exist") + geometry = monitor.get_geometry() + + args.insert(1, "startx={}".format(geometry.x)) + args.insert(2, "starty={}".format(geometry.y)) + args.insert(3, "endx={}".format(geometry.x+geometry.width-1)) + args.insert(4, "endy={}".format(geometry.y+geometry.height-1)) + + print(" ".join(args)) + return args if __name__ == '__main__': screenshare = ScreenShare() diff --git a/sender/service.py b/sender/service.py index 92e1acd..7ca2cd8 100644 --- a/sender/service.py +++ b/sender/service.py @@ -66,6 +66,10 @@ def parameters(self): """ raise NotImplementedError("Pure virtual method called!") + def parse_args(self): + import argparse + argparse.ArgumentParser().parse_args() + def quit(self) -> None: """Close the pipeline""" @@ -119,8 +123,8 @@ def start_transmission(self) -> None: def main(cls, self) -> NoReturn: """Program entry point""" - import argparse, qubesdb, os - argparse.ArgumentParser().parse_args() + import qubesdb, os + self.parse_args() try: target_domain = qubesdb.QubesDB().read('/name').decode('ascii', 'strict') From 803d66b62ff70ae791d3f5fe039b8e19ac423f85 Mon Sep 17 00:00:00 2001 From: jampe Date: Wed, 30 Mar 2022 09:30:45 +0200 Subject: [PATCH 2/4] update readme Signed-off-by: jampe --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d199454..1c242c0 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,14 @@ A secure confirmation dialog will appear asking where the webcam stream is to be Simply run the following command in the virtual machine of the screen sharing recipient: -`qubes-video-companion screenshare` +`qubes-video-companion screenshare [screenId]` A secure confirmation dialog will appear asking where the screen to share is to be sourced from. Select any qube as the target screen, this could be a regular unprivileged qube such as `personal` or a [DisposableVM](https://www.qubes-os.org/doc/disposablevm/), or the ultimately trusted `dom0` (caution is advised to avoid information disclosure. Afterwards, confirm the operation by clicking `OK`. Note that confirmation isn't required when a VM wants to view the screen of a DisposableVM it launched itself because the parent VM already has full control over the DisposableVM. +The command above will share all active monitors in a multi monitor setup per default. You can pass the screenId as optional parameter to share a specific screen only. + ### Preview At this point, install and open an application such as Cheese (packaged as `cheese`) to preview the webcam or screen shared video stream. You're all set! From b5d834f6ff14c8201edc337291d30b32bd99f197 Mon Sep 17 00:00:00 2001 From: jampe Date: Wed, 30 Mar 2022 22:37:00 +0200 Subject: [PATCH 3/4] improve untrusted width / height / fps checks Signed-off-by: jampe --- receiver/receiver.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/receiver/receiver.py b/receiver/receiver.py index 95b5fe0..9079819 100644 --- a/receiver/receiver.py +++ b/receiver/receiver.py @@ -65,8 +65,18 @@ def read_video_parameters() -> (int, int, int): del untrusted_input screen = Gdk.Display().get_default().get_default_screen() - if untrusted_width > screen.width() or untrusted_height > screen.height() or untrusted_fps > 4096: - print('warning: excessive width, height, and/or fps') + if untrusted_width > screen.width() or untrusted_height > screen.height(): + raise RuntimeError('excessive width, height') + + if untrusted_fps > 60: + raise RuntimeError('excessive fps') + + if untrusted_width > (3840 * 3): + raise RuntimeError('excessive width') + + if untrusted_height > (2160 * 3): + raise RuntimeError('excessive height') + width, height, fps = untrusted_width, untrusted_height, untrusted_fps del untrusted_width, untrusted_height, untrusted_fps From 2116a3ba5bcd4fb7b1135a1b7529c23f50a0eb89 Mon Sep 17 00:00:00 2001 From: jampe Date: Thu, 31 Mar 2022 08:52:52 +0200 Subject: [PATCH 4/4] remove print from debug session --- sender/screenshare.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sender/screenshare.py b/sender/screenshare.py index a34b70e..40d0c8d 100644 --- a/sender/screenshare.py +++ b/sender/screenshare.py @@ -37,7 +37,12 @@ def get_specific_monitor(self, monitor_id): def parse_args(self): import argparse parser = argparse.ArgumentParser() - parser.add_argument('screen', type=int, nargs='?', default=None, help='id of the screen to share') + parser.add_argument( + 'screen', + type=int, + nargs='?', + default=None, + help='id of the screen to share') args = parser.parse_args() self._share_specific_screen = args.screen @@ -87,10 +92,9 @@ def pipeline(self, width: int, height: int, fps: int): args.insert(1, "startx={}".format(geometry.x)) args.insert(2, "starty={}".format(geometry.y)) - args.insert(3, "endx={}".format(geometry.x+geometry.width-1)) - args.insert(4, "endy={}".format(geometry.y+geometry.height-1)) + args.insert(3, "endx={}".format(geometry.x + geometry.width -1)) + args.insert(4, "endy={}".format(geometry.y + geometry.height -1)) - print(" ".join(args)) return args if __name__ == '__main__':