Add qubes.VMExec call, for running a single command
With a VMExecGUI variant that waits for a session. See QubesOS/qubes-issues#4850.
This commit is contained in:
parent
c997008e2f
commit
738548a8e4
4
debian/qubes-core-agent.install
vendored
4
debian/qubes-core-agent.install
vendored
@ -30,6 +30,8 @@ etc/qubes-rpc/qubes.SuspendPreAll
|
|||||||
etc/qubes-rpc/qubes.ConnectTCP
|
etc/qubes-rpc/qubes.ConnectTCP
|
||||||
etc/qubes-rpc/qubes.VMShell
|
etc/qubes-rpc/qubes.VMShell
|
||||||
etc/qubes-rpc/qubes.VMRootShell
|
etc/qubes-rpc/qubes.VMRootShell
|
||||||
|
etc/qubes-rpc/qubes.VMExec
|
||||||
|
etc/qubes-rpc/qubes.VMExecGUI
|
||||||
etc/qubes-rpc/qubes.WaitForSession
|
etc/qubes-rpc/qubes.WaitForSession
|
||||||
etc/qubes-rpc/qubes.GetDate
|
etc/qubes-rpc/qubes.GetDate
|
||||||
etc/qubes-suspend-module-blacklist
|
etc/qubes-suspend-module-blacklist
|
||||||
@ -43,6 +45,7 @@ etc/qubes/rpc-config/qubes.SelectDirectory
|
|||||||
etc/qubes/rpc-config/qubes.StartApp
|
etc/qubes/rpc-config/qubes.StartApp
|
||||||
etc/qubes/rpc-config/qubes.InstallUpdatesGUI
|
etc/qubes/rpc-config/qubes.InstallUpdatesGUI
|
||||||
etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
||||||
|
etc/qubes/rpc-config/qubes.VMExecGUI
|
||||||
etc/qubes/suspend-post.d/README
|
etc/qubes/suspend-post.d/README
|
||||||
etc/qubes/suspend-post.d/*.sh
|
etc/qubes/suspend-post.d/*.sh
|
||||||
etc/qubes/suspend-pre.d/README
|
etc/qubes/suspend-pre.d/README
|
||||||
@ -94,6 +97,7 @@ usr/bin/qubes-desktop-run
|
|||||||
usr/bin/qubes-open
|
usr/bin/qubes-open
|
||||||
usr/bin/qubes-session-autostart
|
usr/bin/qubes-session-autostart
|
||||||
usr/bin/qubes-run-terminal
|
usr/bin/qubes-run-terminal
|
||||||
|
usr/bin/qubes-vmexec
|
||||||
usr/bin/qvm-copy
|
usr/bin/qvm-copy
|
||||||
usr/bin/qvm-copy-to-vm
|
usr/bin/qvm-copy-to-vm
|
||||||
usr/bin/qvm-features-request
|
usr/bin/qvm-features-request
|
||||||
|
@ -52,6 +52,7 @@ install:
|
|||||||
install -t $(DESTDIR)$(QUBESRPCCMDDIR) \
|
install -t $(DESTDIR)$(QUBESRPCCMDDIR) \
|
||||||
qubes.Filecopy qubes.OpenInVM qubes.VMShell \
|
qubes.Filecopy qubes.OpenInVM qubes.VMShell \
|
||||||
qubes.VMRootShell \
|
qubes.VMRootShell \
|
||||||
|
qubes.VMExec \
|
||||||
qubes.OpenURL \
|
qubes.OpenURL \
|
||||||
qubes.SuspendPre qubes.SuspendPost qubes.GetAppmenus \
|
qubes.SuspendPre qubes.SuspendPost qubes.GetAppmenus \
|
||||||
qubes.SuspendPreAll \
|
qubes.SuspendPreAll \
|
||||||
@ -69,6 +70,7 @@ install:
|
|||||||
qubes.GetDate \
|
qubes.GetDate \
|
||||||
qubes.ShowInTerminal \
|
qubes.ShowInTerminal \
|
||||||
qubes.ConnectTCP
|
qubes.ConnectTCP
|
||||||
|
ln -s qubes.VMExec $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.VMExecGUI
|
||||||
for config in *.config; do \
|
for config in *.config; do \
|
||||||
install -D -m 0644 "$$config" "$(DESTDIR)$(QUBESRPCCONFDIR)/$${config%.config}"; \
|
install -D -m 0644 "$$config" "$(DESTDIR)$(QUBESRPCCONFDIR)/$${config%.config}"; \
|
||||||
done
|
done
|
||||||
|
3
qubes-rpc/qubes.VMExec
Executable file
3
qubes-rpc/qubes.VMExec
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
exec /usr/bin/qubes-vmexec "$@"
|
1
qubes-rpc/qubes.VMExecGUI.config
Normal file
1
qubes-rpc/qubes.VMExecGUI.config
Normal file
@ -0,0 +1 @@
|
|||||||
|
wait-for-session=1
|
52
qubesagent/test_vmexec.py
Normal file
52
qubesagent/test_vmexec.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
|
from qubesagent.vmexec import main, decode, DecodeError
|
||||||
|
|
||||||
|
|
||||||
|
class TestVmExec(TestCase):
|
||||||
|
def test_00_decode_simple(self):
|
||||||
|
self.assertEqual(decode('echo+Hello'), [b'echo', b'Hello'])
|
||||||
|
|
||||||
|
def test_01_decode_empty(self):
|
||||||
|
self.assertEqual(decode('echo+'), [b'echo', b''])
|
||||||
|
|
||||||
|
def test_02_decode_escaping(self):
|
||||||
|
self.assertEqual(decode('echo+Hello-20world'),
|
||||||
|
[b'echo', b'Hello world'])
|
||||||
|
self.assertEqual(decode('-0A-0D'),
|
||||||
|
[b'\n\r'])
|
||||||
|
self.assertEqual(decode('-2Fbin-2Fls'),
|
||||||
|
[b'/bin/ls'])
|
||||||
|
self.assertEqual(decode('ls+--la'),
|
||||||
|
[b'ls', b'-la'])
|
||||||
|
self.assertEqual(decode('ls+---61'),
|
||||||
|
[b'ls', b'-a'])
|
||||||
|
self.assertEqual(decode('ls+----help'),
|
||||||
|
[b'ls', b'--help'])
|
||||||
|
|
||||||
|
def test_03_decode_errors(self):
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('illegal/slash')
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('illegal-singledash')
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('smalletters-0a-0d')
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('incompletebyte-A')
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('incomplete-Abyte')
|
||||||
|
with self.assertRaises(DecodeError):
|
||||||
|
decode('ls+---threeslashes')
|
||||||
|
|
||||||
|
def test_10_main_exec(self):
|
||||||
|
with patch('os.execvp') as mock_execvp:
|
||||||
|
main(['vmexec', 'ls+--la'])
|
||||||
|
self.assertEqual(mock_execvp.call_args_list, [
|
||||||
|
call(b'ls', [b'ls', b'-la'])])
|
||||||
|
|
||||||
|
def test_11_main_fail(self):
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
main(['vmexec'])
|
||||||
|
with self.assertRaises(SystemExit):
|
||||||
|
main(['vmexec', 'illegal/slash'])
|
59
qubesagent/vmexec.py
Normal file
59
qubesagent/vmexec.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class DecodeError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ESCAPE_RE = re.compile(br'--|-([A-F0-9]{2})')
|
||||||
|
|
||||||
|
|
||||||
|
def decode_part(part):
|
||||||
|
if not re.match(r'^[a-zA-Z0-9._-]*$', part):
|
||||||
|
raise DecodeError('illegal characters found')
|
||||||
|
|
||||||
|
part = part.encode('ascii')
|
||||||
|
|
||||||
|
# Check if no '-' remains outside of legal escape sequences.
|
||||||
|
if b'-' in ESCAPE_RE.sub(b'', part):
|
||||||
|
raise DecodeError("'-' can be used only in '-HH' or '--'")
|
||||||
|
|
||||||
|
def convert(m):
|
||||||
|
if m.group(0) == b'--':
|
||||||
|
return b'-'
|
||||||
|
num = int(m.group(1), 16)
|
||||||
|
return bytes([num])
|
||||||
|
|
||||||
|
return ESCAPE_RE.sub(convert, part)
|
||||||
|
|
||||||
|
|
||||||
|
def decode(arg):
|
||||||
|
'''
|
||||||
|
Decode the argument for executing. The format is as follows:
|
||||||
|
- individual parts are split by '+'
|
||||||
|
- bytes are escaped as '-HH' (where HH is hex code, capital letters only)
|
||||||
|
- literal '-' is encoded as '--'
|
||||||
|
- otherwise, only [a-zA-Z0-9._] are allowed
|
||||||
|
|
||||||
|
:param arg: argument, as a string
|
||||||
|
:returns: list of exec arguments (each as bytes)
|
||||||
|
'''
|
||||||
|
return [decode_part(part) for part in arg.split('+')]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv) != 2:
|
||||||
|
print('This service requires exactly one argument', file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
try:
|
||||||
|
command = decode(argv[1])
|
||||||
|
except DecodeError as e:
|
||||||
|
print('Decode error: {}'.format(e), file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
os.execvp(command[0], command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -549,6 +549,8 @@ rm -f %{name}-%{version}
|
|||||||
%config(noreplace) /etc/qubes-rpc/qubes.GetAppmenus
|
%config(noreplace) /etc/qubes-rpc/qubes.GetAppmenus
|
||||||
%config(noreplace) /etc/qubes-rpc/qubes.ConnectTCP
|
%config(noreplace) /etc/qubes-rpc/qubes.ConnectTCP
|
||||||
%config(noreplace) /etc/qubes-rpc/qubes.VMShell
|
%config(noreplace) /etc/qubes-rpc/qubes.VMShell
|
||||||
|
%config(noreplace) /etc/qubes-rpc/qubes.VMExec
|
||||||
|
%config(noreplace) /etc/qubes-rpc/qubes.VMExecGUI
|
||||||
%config(noreplace) /etc/qubes-rpc/qubes.VMRootShell
|
%config(noreplace) /etc/qubes-rpc/qubes.VMRootShell
|
||||||
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPre
|
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPre
|
||||||
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPreAll
|
%config(noreplace) /etc/qubes-rpc/qubes.SuspendPreAll
|
||||||
@ -574,6 +576,7 @@ rm -f %{name}-%{version}
|
|||||||
%config(noreplace) /etc/qubes/rpc-config/qubes.StartApp
|
%config(noreplace) /etc/qubes/rpc-config/qubes.StartApp
|
||||||
%config(noreplace) /etc/qubes/rpc-config/qubes.InstallUpdatesGUI
|
%config(noreplace) /etc/qubes/rpc-config/qubes.InstallUpdatesGUI
|
||||||
%config(noreplace) /etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
%config(noreplace) /etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
||||||
|
%config(noreplace) /etc/qubes/rpc-config/qubes.VMExecGUI
|
||||||
%dir /etc/qubes/autostart
|
%dir /etc/qubes/autostart
|
||||||
%config(noreplace) /etc/default/grub.qubes
|
%config(noreplace) /etc/default/grub.qubes
|
||||||
/etc/qubes/autostart/README.txt
|
/etc/qubes/autostart/README.txt
|
||||||
@ -616,6 +619,7 @@ rm -f %{name}-%{version}
|
|||||||
/usr/bin/qubes-session-autostart
|
/usr/bin/qubes-session-autostart
|
||||||
/usr/bin/qvm-console
|
/usr/bin/qvm-console
|
||||||
/usr/bin/qvm-connect-tcp
|
/usr/bin/qvm-connect-tcp
|
||||||
|
/usr/bin/qubes-vmexec
|
||||||
%dir /usr/lib/qubes
|
%dir /usr/lib/qubes
|
||||||
/usr/lib/qubes/prepare-suspend
|
/usr/lib/qubes/prepare-suspend
|
||||||
/usr/lib/qubes/qfile-agent
|
/usr/lib/qubes/qfile-agent
|
||||||
@ -667,6 +671,8 @@ rm -f %{name}-%{version}
|
|||||||
%{python3_sitelib}/qubesagent/__init__.py*
|
%{python3_sitelib}/qubesagent/__init__.py*
|
||||||
%{python3_sitelib}/qubesagent/firewall.py*
|
%{python3_sitelib}/qubesagent/firewall.py*
|
||||||
%{python3_sitelib}/qubesagent/test_firewall.py*
|
%{python3_sitelib}/qubesagent/test_firewall.py*
|
||||||
|
%{python3_sitelib}/qubesagent/vmexec.py*
|
||||||
|
%{python3_sitelib}/qubesagent/test_vmexec.py*
|
||||||
%{python3_sitelib}/qubesagent/xdg.py*
|
%{python3_sitelib}/qubesagent/xdg.py*
|
||||||
|
|
||||||
/usr/share/qubes/mime-override/globs
|
/usr/share/qubes/mime-override/globs
|
||||||
|
Loading…
Reference in New Issue
Block a user