diff --git a/qubes-rpc/services/qvc.Webcam b/qubes-rpc/services/qvc.Webcam index 1546eb4..7878428 100755 --- a/qubes-rpc/services/qvc.Webcam +++ b/qubes-rpc/services/qvc.Webcam @@ -4,4 +4,4 @@ # Copyright (C) 2021 Demi Marie Obenour # Licensed under the MIT License. See LICENSE file for details. export DISPLAY=:0 -exec python3 -- /usr/share/qubes-video-companion/sender/webcam.py +exec python3 -- /usr/share/qubes-video-companion/sender/webcam.py ${1:+"$1"} diff --git a/receiver/qubes-video-companion b/receiver/qubes-video-companion index a32b0fd..6c7d310 100755 --- a/receiver/qubes-video-companion +++ b/receiver/qubes-video-companion @@ -4,18 +4,39 @@ # Licensed under the MIT License. See LICENSE file for details. [ "$DEBUG" == 1 ] && set -x +set -u -e set -E # Enable function inheritance of traps trap exit ERR +unset GETOPT_COMPATIBLE +name=${0##*/} usage() { - echo "Usage: qubes-video-companion webcam|screenshare [destination qube]" -} >&2 + printf '%s: Usage: qubes-video-companion [--resolution=WIDTHxHEIGHTxFPS] [--] webcam|screenshare [destination qube]\n' "$name" + exit "$1" +} -if [ "$#" -gt 2 ] && [ "$#" -lt 1 ]; then - usage - exit 1 -fi +resolution= +opts=$(getopt "--name=$name" --longoptions=resolution:,help -- r: "$@") || exit +eval "set -- $opts" +while :; do + case $1 in + -r|--resolution) + if [[ -z "$2" ]]; then + printf '%s: Empty resolution argument\n' "$name" + usage 0 + fi >&2 + resolution=${2//@/+} + resolution=${resolution//x/+} + shift 2 + ;; + --help) usage 0;; + --) shift; break;; + *) exit 1;; # cannot happen + esac +done + +if [ "$#" -gt 2 ] || [ "$#" -lt 1 ]; then usage 1 >&2; fi video_source="$1" qube=${2-'@default'} @@ -27,18 +48,17 @@ case "$video_source" in qvc_service="qvc.ScreenShare" ;; *) - usage - exit 1 + usage 1 ;; esac -exit_clean() { +exit_clean () { exit_code="$?" /usr/share/qubes-video-companion/receiver/destroy.sh sudo rm -f "$qvc_lock_file" - if [ "$video_source" == "webcam" ] && [ "$exit_code" == "141" ]; then + if [ "$video_source" = "webcam" ] && [ "$exit_code" = "141" ]; then echo "The webcam device is in use! Please stop any instance of Qubes Video Companion running on another qube." >&2 exit 1 fi @@ -57,4 +77,4 @@ fi /usr/share/qubes-video-companion/receiver/setup.sh # Filter standard error escape characters for safe printing to the terminal from the video sender -qrexec-client-vm --filter-escape-chars-stderr -- "$qube" "$qvc_service" /usr/share/qubes-video-companion/receiver/receiver.py +qrexec-client-vm --filter-escape-chars-stderr -- "$qube" "$qvc_service+$resolution" /usr/share/qubes-video-companion/receiver/receiver.py diff --git a/sender/service.py b/sender/service.py index 39acbd1..98e8399 100644 --- a/sender/service.py +++ b/sender/service.py @@ -137,11 +137,8 @@ def start_transmission(self) -> None: def main(cls, self) -> NoReturn: """Program entry point""" - import argparse import qubesdb # pylint: disable=import-error - argparse.ArgumentParser().parse_args() - target_domain = qubesdb.QubesDB().read("/name") if target_domain is None: # dom0 doesn't have a /name value in its QubesDB diff --git a/sender/webcam.py b/sender/webcam.py index 0596d4e..c63fd30 100644 --- a/sender/webcam.py +++ b/sender/webcam.py @@ -15,8 +15,43 @@ class Webcam(Service): """Webcam video source class""" - def __init__(self): - self.main(self) + untrusted_requested_width: int + untrusted_requested_height: int + untrusted_requested_fps: int + + def __init__(self, *, untrusted_arg: str): + if untrusted_arg: + untrusted_arg_bytes = untrusted_arg.encode('ascii', 'strict') + def parse_int(untrusted_decimal: bytes) -> int: + if not ((1 <= len(untrusted_decimal) <= 4) and + untrusted_decimal.isdigit() and + untrusted_decimal[0] != b"0"): + print("Invalid argument " + untrusted_arg + ": bad number", + file=sys.stderr) + sys.exit(1) + return int(untrusted_decimal, 10) + if len(untrusted_arg_bytes) > 14: + # qrexec has already sanitized the argument to some degree, + # so this is safe + print("Invalid argument " + untrusted_arg + + ": too long (limit 14 bytes)", file=sys.stderr) + sys.exit(1) + arg_list = untrusted_arg_bytes.split(b"+", 4) + if len(arg_list) != 3: + print("Invalid argument " + untrusted_arg + + ": wrong number of integers (expected 3)", + file=sys.stderr) + sys.exit(1) + ( self.untrusted_requested_width + , self.untrusted_requested_height + , self.untrusted_requested_fps + ) = map(parse_int, arg_list) + else: + self.untrusted_requested_width = 0 + self.untrusted_requested_height = 0 + self.untrusted_requested_fps = 0 + + Service.main(self) def video_source(self) -> str: return "webcam" @@ -60,6 +95,11 @@ def parameters(self): else: print("Cannot parse output %r of v4l2ctl" % i, file=sys.stderr) formats.sort(key=lambda x: x[0] * x[1] * x[2], reverse=True) + if self.untrusted_requested_fps: + formats.sort(key=lambda x: + (x[0] - self.untrusted_requested_width) ** 2 + + (x[1] - self.untrusted_requested_height) ** 2 + + (x[2] - self.untrusted_requested_fps) ** 2) return formats[0] def pipeline(self, width: int, height: int, fps: int, **kwargs): @@ -108,4 +148,11 @@ def pipeline(self, width: int, height: int, fps: int, **kwargs): if __name__ == "__main__": - webcam = Webcam() + _untrusted_arg = "" + if len(sys.argv) == 2: + _untrusted_arg = sys.argv[1] + elif len(sys.argv) != 1: + print("Must have 0 or 1 argument, not " + str(len(sys.argv)), + file=sys.stderr) + sys.exit(1) + webcam = Webcam(untrusted_arg=_untrusted_arg)