diff --git a/.travis.yml b/.travis.yml index 2e11ea154..d957f4435 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ services: install: - sudo apt-get -y install python3-gi gir1.2-gtk-3.0 - pip3 install --quiet -r ci/requirements.txt - - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-builder ~/qubes-builder - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-core-qrexec ~/qubes-core-qrexec script: - PYTHONPATH=test-packages:~/qubes-core-qrexec pylint qubes diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 9ff81651b..e6fe1a059 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -21,8 +21,6 @@ # License along with this library; if not, see . # -import re - import qubes.config import qubes.ext import qubes.exc @@ -93,10 +91,8 @@ def on_domain_qdb_create(self, vm, event): vm.untrusted_qdb.write('/qubes-gui-domain-xid', str(vm.guivm.xid)) - # Add keyboard layout from that of GuiVM - kbd_layout = vm.guivm.features.get('keyboard-layout', None) - if kbd_layout: - vm.untrusted_qdb.write('/keyboard-layout', kbd_layout) + # Add keyboard layout + vm.untrusted_qdb.write('/keyboard-layout', vm.keyboard_layout) # Set GuiVM prefix guivm_windows_prefix = vm.features.get('guivm-windows-prefix', 'GuiVM') @@ -121,22 +117,15 @@ def on_domain_start(self, vm, event, **kwargs): attached_vm.untrusted_qdb.write('/qubes-gui-domain-xid', str(vm.xid)) - @qubes.ext.handler('domain-feature-pre-set:keyboard-layout') - def on_feature_pre_set(self, subject, event, feature, value, oldvalue=None): - untrusted_xkb_layout = value.split('+') - if len(untrusted_xkb_layout) != 3: - raise qubes.exc.QubesValueError("Invalid number of parameters") - - untrusted_layout = untrusted_xkb_layout[0] - untrusted_variant = untrusted_xkb_layout[1] - untrusted_options = untrusted_xkb_layout[2] - - re_variant = r'^[a-zA-Z0-9-_]*$' - re_options = r'^[a-zA-Z0-9-_:,]*$' - - if not untrusted_layout.isalpha(): - raise qubes.exc.QubesValueError("Invalid layout provided") - if not re.match(re_variant, untrusted_variant): - raise qubes.exc.QubesValueError("Invalid variant provided") - if not re.match(re_options, untrusted_options): - raise qubes.exc.QubesValueError("Invalid options provided") + @qubes.ext.handler('property-reset:keyboard_layout') + def on_keyboard_reset(self, subject, event, name, oldvalue=None): + if getattr(subject, 'guivm'): + kbd_layout = subject.guivm.keyboard_layout + else: + kbd_layout = 'us++' + + subject.untrusted_qdb.write('/keyboard-layout', kbd_layout) + + @qubes.ext.handler('property-set:keyboard_layout') + def on_keyboard_set(self, subject, event, name, newvalue, oldvalue=None): + subject.untrusted_qdb.write('/keyboard-layout', newvalue) diff --git a/qubes/tests/vm/__init__.py b/qubes/tests/vm/__init__.py index 9850a4593..afecfb347 100644 --- a/qubes/tests/vm/__init__.py +++ b/qubes/tests/vm/__init__.py @@ -99,6 +99,7 @@ def __init__(self): self.default_pool_kernel = 'linux-kernel' self.default_qrexec_timeout = 60 self.default_netvm = None + self.default_guivm = None self.domains = TestVMsCollection() #: jinja2 environment for libvirt XML templates self.env = jinja2.Environment( diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 3d3c52342..37918e187 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1696,6 +1696,7 @@ def test_620_qdb_standalone(self, mock_qubesdb, mock_urandom, '/name': 'test-inst-test', '/type': 'StandaloneVM', '/default-user': 'user', + '/keyboard-layout': 'us++', '/qubes-vm-type': 'AppVM', '/qubes-debug-mode': '0', '/qubes-base-template': '', @@ -1762,6 +1763,7 @@ def test_621_qdb_vm_with_network(self, mock_qubesdb, mock_urandom, '/name': 'test-inst-appvm', '/type': 'AppVM', '/default-user': 'user', + '/keyboard-layout': 'us++', '/qubes-vm-type': 'AppVM', '/qubes-debug-mode': '0', '/qubes-base-template': 'test-inst-template', @@ -1877,7 +1879,7 @@ def test_622_qdb_guivm_keyboard_layout(self, mock_qubesdb, mock_urandom, name='appvm', qid=3) vm.netvm = None vm.guivm = guivm - guivm.features['keyboard-layout'] = 'fr++' + guivm.keyboard_layout = 'fr++' guivm.is_running = lambda: True vm.events_enabled = True test_qubesdb = TestQubesDB() @@ -1936,6 +1938,7 @@ def test_623_qdb_audiovm(self, mock_qubesdb, mock_urandom, '/name': 'test-inst-appvm', '/type': 'AppVM', '/default-user': 'user', + '/keyboard-layout': 'us++', '/qubes-vm-type': 'AppVM', '/qubes-audio-domain-xid': '{}'.format(audiovm.xid), '/qubes-debug-mode': '0', @@ -1969,7 +1972,75 @@ def test_624_qdb_guivm_invalid_keyboard_layout(self, mock_qubesdb, guivm.is_running = lambda: True guivm.events_enabled = True with self.assertRaises(qubes.exc.QubesValueError): - guivm.features['keyboard-layout'] = 'fr123++' + guivm.keyboard_layout = 'fr123++' + + @unittest.mock.patch('qubes.utils.get_timezone') + @unittest.mock.patch('qubes.utils.urandom') + @unittest.mock.patch('qubes.vm.qubesvm.QubesVM.untrusted_qdb') + def test_625_qdb_keyboard_layout_change(self, mock_qubesdb, mock_urandom, + mock_timezone): + mock_urandom.return_value = b'A' * 64 + mock_timezone.return_value = 'UTC' + template = self.get_vm( + cls=qubes.vm.templatevm.TemplateVM, name='template') + template.netvm = None + guivm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template, + name='sys-gui', qid=2, provides_network=False) + vm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template, + name='appvm', qid=3) + vm.netvm = None + vm.guivm = guivm + guivm.keyboard_layout = 'fr++' + guivm.is_running = lambda: True + vm.events_enabled = True + test_qubesdb = TestQubesDB() + mock_qubesdb.write.side_effect = test_qubesdb.write + mock_qubesdb.rm.side_effect = test_qubesdb.rm + vm.create_qdb_entries() + self.maxDiff = None + + expected = { + '/name': 'test-inst-appvm', + '/type': 'AppVM', + '/default-user': 'user', + '/keyboard-layout': 'fr++', + '/qubes-vm-type': 'AppVM', + '/qubes-gui-domain-xid': '{}'.format(guivm.xid), + '/qubes-debug-mode': '0', + '/qubes-base-template': 'test-inst-template', + '/qubes-timezone': 'UTC', + '/qubes-random-seed': base64.b64encode(b'A' * 64), + '/qubes-vm-persistence': 'rw-only', + '/qubes-vm-updateable': 'False', + '/qubes-block-devices': '', + '/qubes-usb-devices': '', + '/qubes-iptables': 'reload', + '/qubes-iptables-error': '', + '/qubes-iptables-header': unittest.mock.ANY, + '/qubes-service/qubes-update-check': '0', + '/qubes-service/meminfo-writer': '1', + '/connected-ips': '', + '/connected-ips6': '', + } + + with self.subTest('default'): + vm.create_qdb_entries() + self.assertEqual(test_qubesdb.data, expected) + + test_qubesdb.data.clear() + with self.subTest('value_change'): + vm.keyboard_layout = 'de++' + expected['/keyboard-layout'] = 'de++' + vm.create_qdb_entries() + self.assertEqual(test_qubesdb.data, expected) + + test_qubesdb.data.clear() + with self.subTest('value_revert'): + vm.keyboard_layout = qubes.property.DEFAULT + expected['/keyboard-layout'] = 'fr++' + vm.create_qdb_entries() + self.assertEqual(test_qubesdb.data, expected) + @asyncio.coroutine def coroutine_mock(self, mock, *args, **kwargs): diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index f02021c3a..789e66f35 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -28,6 +28,7 @@ import qubes import qubes.exc import qubes.vm +from qubes.vm.qubesvm import _setter_kbd_layout class AdminVM(qubes.vm.BaseVM): @@ -61,6 +62,13 @@ class AdminVM(qubes.vm.BaseVM): setter=qubes.property.forbidden, doc='True if this machine may be updated on its own.') + # for changes in keyboard_layout, see also the same property in QubesVM + keyboard_layout = qubes.property( + 'keyboard_layout', + type=str, + setter=_setter_kbd_layout, + doc='Keyboard layout for this VM') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index aca16905e..55f0f93f6 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -23,6 +23,7 @@ import asyncio import base64 import grp +import re import os import os.path import shutil @@ -115,6 +116,32 @@ def _setter_virt_mode(self, prop, value): return value +def _setter_kbd_layout(self, prop, value): + untrusted_xkb_layout = value.split('+') + if len(untrusted_xkb_layout) != 3: + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "invalid number of keyboard layout parameters") + + untrusted_layout = untrusted_xkb_layout[0] + untrusted_variant = untrusted_xkb_layout[1] + untrusted_options = untrusted_xkb_layout[2] + + re_variant = r'^[a-zA-Z0-9-_]*$' + re_options = r'^[a-zA-Z0-9-_:,]*$' + + if not untrusted_layout.isalpha(): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid keyboard layout provided") + if not re.match(re_variant, untrusted_variant): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid laoyout variant provided") + if not re.match(re_options, untrusted_options): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid layout options provided") + + return value + + def _default_virt_mode(self): if self.devices['pci'].persistent(): return 'hvm' @@ -690,6 +717,14 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): setter=qubes.property.forbidden, doc='True if this machine may be updated on its own.') + # for changes in keyboard_layout, see also the same property in AdminVM + keyboard_layout = qubes.property( + 'keyboard_layout', + default=(lambda self: getattr(self.guivm, 'keyboard_layout', 'us++')), + type=str, + setter=_setter_kbd_layout, + doc='Keyboard layout for this VM') + # # static, class-wide properties #