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.VMShell
|
||||
etc/qubes-rpc/qubes.VMRootShell
|
||||
etc/qubes-rpc/qubes.VMExec
|
||||
etc/qubes-rpc/qubes.VMExecGUI
|
||||
etc/qubes-rpc/qubes.WaitForSession
|
||||
etc/qubes-rpc/qubes.GetDate
|
||||
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.InstallUpdatesGUI
|
||||
etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
||||
etc/qubes/rpc-config/qubes.VMExecGUI
|
||||
etc/qubes/suspend-post.d/README
|
||||
etc/qubes/suspend-post.d/*.sh
|
||||
etc/qubes/suspend-pre.d/README
|
||||
@ -94,6 +97,7 @@ usr/bin/qubes-desktop-run
|
||||
usr/bin/qubes-open
|
||||
usr/bin/qubes-session-autostart
|
||||
usr/bin/qubes-run-terminal
|
||||
usr/bin/qubes-vmexec
|
||||
usr/bin/qvm-copy
|
||||
usr/bin/qvm-copy-to-vm
|
||||
usr/bin/qvm-features-request
|
||||
|
@ -52,6 +52,7 @@ install:
|
||||
install -t $(DESTDIR)$(QUBESRPCCMDDIR) \
|
||||
qubes.Filecopy qubes.OpenInVM qubes.VMShell \
|
||||
qubes.VMRootShell \
|
||||
qubes.VMExec \
|
||||
qubes.OpenURL \
|
||||
qubes.SuspendPre qubes.SuspendPost qubes.GetAppmenus \
|
||||
qubes.SuspendPreAll \
|
||||
@ -69,6 +70,7 @@ install:
|
||||
qubes.GetDate \
|
||||
qubes.ShowInTerminal \
|
||||
qubes.ConnectTCP
|
||||
ln -s qubes.VMExec $(DESTDIR)$(QUBESRPCCMDDIR)/qubes.VMExecGUI
|
||||
for config in *.config; do \
|
||||
install -D -m 0644 "$$config" "$(DESTDIR)$(QUBESRPCCONFDIR)/$${config%.config}"; \
|
||||
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.ConnectTCP
|
||||
%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.SuspendPre
|
||||
%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.InstallUpdatesGUI
|
||||
%config(noreplace) /etc/qubes/rpc-config/qubes.VMShell+WaitForSession
|
||||
%config(noreplace) /etc/qubes/rpc-config/qubes.VMExecGUI
|
||||
%dir /etc/qubes/autostart
|
||||
%config(noreplace) /etc/default/grub.qubes
|
||||
/etc/qubes/autostart/README.txt
|
||||
@ -616,6 +619,7 @@ rm -f %{name}-%{version}
|
||||
/usr/bin/qubes-session-autostart
|
||||
/usr/bin/qvm-console
|
||||
/usr/bin/qvm-connect-tcp
|
||||
/usr/bin/qubes-vmexec
|
||||
%dir /usr/lib/qubes
|
||||
/usr/lib/qubes/prepare-suspend
|
||||
/usr/lib/qubes/qfile-agent
|
||||
@ -667,6 +671,8 @@ rm -f %{name}-%{version}
|
||||
%{python3_sitelib}/qubesagent/__init__.py*
|
||||
%{python3_sitelib}/qubesagent/firewall.py*
|
||||
%{python3_sitelib}/qubesagent/test_firewall.py*
|
||||
%{python3_sitelib}/qubesagent/vmexec.py*
|
||||
%{python3_sitelib}/qubesagent/test_vmexec.py*
|
||||
%{python3_sitelib}/qubesagent/xdg.py*
|
||||
|
||||
/usr/share/qubes/mime-override/globs
|
||||
|
Loading…
Reference in New Issue
Block a user