mime.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2016 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This library is free software; you can redistribute it and/or
  10. # modify it under the terms of the GNU Lesser General Public
  11. # License as published by the Free Software Foundation; either
  12. # version 2.1 of the License, or (at your option) any later version.
  13. #
  14. # This library is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  17. # Lesser General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Lesser General Public
  20. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  21. #
  22. #
  23. from distutils import spawn
  24. import os
  25. import re
  26. import subprocess
  27. import time
  28. import unittest
  29. import qubes.tests
  30. import qubes.qubes
  31. from qubes.qubes import QubesVmCollection
  32. @unittest.skipUnless(
  33. spawn.find_executable('xprop') and
  34. spawn.find_executable('xdotool') and
  35. spawn.find_executable('wmctrl'),
  36. "xprop or xdotool or wmctrl not installed")
  37. class TC_50_MimeHandlers(qubes.tests.SystemTestsMixin):
  38. @classmethod
  39. def setUpClass(cls):
  40. if cls.template == 'whonix-gw' or 'minimal' in cls.template:
  41. raise unittest.SkipTest(
  42. 'Template {} not supported by this test'.format(cls.template))
  43. if cls.template == 'whonix-ws':
  44. # TODO remove when Whonix-based DispVMs will work (Whonix 13?)
  45. raise unittest.SkipTest(
  46. 'Template {} not supported by this test'.format(cls.template))
  47. qc = QubesVmCollection()
  48. cls._kill_test_vms(qc, prefix=qubes.tests.CLSVMPREFIX)
  49. qc.lock_db_for_writing()
  50. qc.load()
  51. cls._remove_test_vms(qc, qubes.qubes.vmm.libvirt_conn,
  52. prefix=qubes.tests.CLSVMPREFIX)
  53. cls.source_vmname = cls.make_vm_name('source', True)
  54. source_vm = qc.add_new_vm("QubesAppVm",
  55. template=qc.get_vm_by_name(cls.template),
  56. name=cls.source_vmname)
  57. source_vm.create_on_disk(verbose=False)
  58. cls.target_vmname = cls.make_vm_name('target', True)
  59. target_vm = qc.add_new_vm("QubesAppVm",
  60. template=qc.get_vm_by_name(cls.template),
  61. name=cls.target_vmname)
  62. target_vm.create_on_disk(verbose=False)
  63. qc.save()
  64. qc.unlock_db()
  65. source_vm.start()
  66. target_vm.start()
  67. # make sure that DispVMs will be started of the same template
  68. retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
  69. cls.template],
  70. stderr=open(os.devnull, 'w'))
  71. assert retcode == 0, "Error preparing DispVM"
  72. def setUp(self):
  73. super(TC_50_MimeHandlers, self).setUp()
  74. self.source_vm = self.qc.get_vm_by_name(self.source_vmname)
  75. self.target_vm = self.qc.get_vm_by_name(self.target_vmname)
  76. def get_window_class(self, winid, dispvm=False):
  77. (vm_winid, _) = subprocess.Popen(
  78. ['xprop', '-id', winid, '_QUBES_VMWINDOWID'],
  79. stdout=subprocess.PIPE
  80. ).communicate()
  81. vm_winid = vm_winid.split("#")[1].strip('\n" ')
  82. if dispvm:
  83. (vmname, _) = subprocess.Popen(
  84. ['xprop', '-id', winid, '_QUBES_VMNAME'],
  85. stdout=subprocess.PIPE
  86. ).communicate()
  87. vmname = vmname.split("=")[1].strip('\n" ')
  88. window_class = None
  89. while window_class is None:
  90. # XXX to use self.qc.get_vm_by_name would require reloading
  91. # qubes.xml, so use qvm-run instead
  92. xprop = subprocess.Popen(
  93. ['qvm-run', '-p', vmname, 'xprop -id {} WM_CLASS'.format(
  94. vm_winid)], stdout=subprocess.PIPE)
  95. (window_class, _) = xprop.communicate()
  96. if xprop.returncode != 0:
  97. self.skipTest("xprop failed, not installed?")
  98. if 'not found' in window_class:
  99. # WM_CLASS not set yet, wait a little
  100. time.sleep(0.1)
  101. window_class = None
  102. else:
  103. window_class = None
  104. while window_class is None:
  105. xprop = self.target_vm.run(
  106. 'xprop -id {} WM_CLASS'.format(vm_winid),
  107. passio_popen=True)
  108. (window_class, _) = xprop.communicate()
  109. if xprop.returncode != 0:
  110. self.skipTest("xprop failed, not installed?")
  111. if 'not found' in window_class:
  112. # WM_CLASS not set yet, wait a little
  113. time.sleep(0.1)
  114. window_class = None
  115. # output: WM_CLASS(STRING) = "gnome-terminal-server", "Gnome-terminal"
  116. try:
  117. window_class = window_class.split("=")[1].split(",")[0].strip('\n" ')
  118. except IndexError:
  119. raise Exception(
  120. "Unexpected output from xprop: '{}'".format(window_class))
  121. return window_class
  122. def open_file_and_check_viewer(self, filename, expected_app_titles,
  123. expected_app_classes, dispvm=False):
  124. self.qc.unlock_db()
  125. if dispvm:
  126. p = self.source_vm.run("qvm-open-in-dvm {}".format(filename),
  127. passio_popen=True)
  128. vmpattern = "disp*"
  129. else:
  130. self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
  131. self.target_vmname)
  132. self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
  133. self.target_vmname)
  134. p = self.source_vm.run("qvm-open-in-vm {} {}".format(
  135. self.target_vmname, filename), passio_popen=True)
  136. vmpattern = self.target_vmname
  137. wait_count = 0
  138. winid = None
  139. window_title = None
  140. while True:
  141. search = subprocess.Popen(['xdotool', 'search',
  142. '--onlyvisible', '--class', vmpattern],
  143. stdout=subprocess.PIPE,
  144. stderr=open(os.path.devnull, 'w'))
  145. retcode = search.wait()
  146. if retcode == 0:
  147. winid = search.stdout.read().strip()
  148. # get window title
  149. (window_title, _) = subprocess.Popen(
  150. ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
  151. communicate()
  152. window_title = window_title.strip()
  153. # ignore LibreOffice splash screen and window with no title
  154. # set yet
  155. if window_title and not window_title.startswith("LibreOffice")\
  156. and not window_title == 'VMapp command':
  157. break
  158. wait_count += 1
  159. if wait_count > 100:
  160. self.fail("Timeout while waiting for editor window")
  161. time.sleep(0.3)
  162. # get window class
  163. window_class = self.get_window_class(winid, dispvm)
  164. # close the window - we've got the window class, it is no longer needed
  165. subprocess.check_call(['wmctrl', '-i', '-c', winid])
  166. p.wait()
  167. self.wait_for_window(window_title, show=False)
  168. def check_matches(obj, patterns):
  169. return any((pat.search(obj) if isinstance(pat, type(re.compile('')))
  170. else pat in obj) for pat in patterns)
  171. if not check_matches(window_title, expected_app_titles) and \
  172. not check_matches(window_class, expected_app_classes):
  173. self.fail("Opening file {} resulted in window '{} ({})', which is "
  174. "none of {!r} ({!r})".format(
  175. filename, window_title, window_class,
  176. expected_app_titles, expected_app_classes))
  177. def prepare_txt(self, filename):
  178. p = self.source_vm.run("cat > {}".format(filename), passio_popen=True)
  179. p.stdin.write("This is test\n")
  180. p.stdin.close()
  181. retcode = p.wait()
  182. assert retcode == 0, "Failed to write {} file".format(filename)
  183. def prepare_pdf(self, filename):
  184. self.prepare_txt("/tmp/source.txt")
  185. cmd = "convert /tmp/source.txt {}".format(filename)
  186. retcode = self.source_vm.run(cmd, wait=True)
  187. assert retcode == 0, "Failed to run '{}'".format(cmd)
  188. def prepare_doc(self, filename):
  189. self.prepare_txt("/tmp/source.txt")
  190. cmd = "unoconv -f doc -o {} /tmp/source.txt".format(filename)
  191. retcode = self.source_vm.run(cmd, wait=True)
  192. if retcode != 0:
  193. self.skipTest("Failed to run '{}', not installed?".format(cmd))
  194. def prepare_pptx(self, filename):
  195. self.prepare_txt("/tmp/source.txt")
  196. cmd = "unoconv -f pptx -o {} /tmp/source.txt".format(filename)
  197. retcode = self.source_vm.run(cmd, wait=True)
  198. if retcode != 0:
  199. self.skipTest("Failed to run '{}', not installed?".format(cmd))
  200. def prepare_png(self, filename):
  201. self.prepare_txt("/tmp/source.txt")
  202. cmd = "convert /tmp/source.txt {}".format(filename)
  203. retcode = self.source_vm.run(cmd, wait=True)
  204. if retcode != 0:
  205. self.skipTest("Failed to run '{}', not installed?".format(cmd))
  206. def prepare_jpg(self, filename):
  207. self.prepare_txt("/tmp/source.txt")
  208. cmd = "convert /tmp/source.txt {}".format(filename)
  209. retcode = self.source_vm.run(cmd, wait=True)
  210. if retcode != 0:
  211. self.skipTest("Failed to run '{}', not installed?".format(cmd))
  212. def test_000_txt(self):
  213. filename = "/home/user/test_file.txt"
  214. self.prepare_txt(filename)
  215. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  216. ["gedit", "emacs", "libreoffice"])
  217. def test_001_pdf(self):
  218. filename = "/home/user/test_file.pdf"
  219. self.prepare_pdf(filename)
  220. self.open_file_and_check_viewer(filename, [],
  221. ["evince"])
  222. def test_002_doc(self):
  223. filename = "/home/user/test_file.doc"
  224. self.prepare_doc(filename)
  225. self.open_file_and_check_viewer(filename, [],
  226. ["libreoffice", "abiword"])
  227. def test_003_pptx(self):
  228. filename = "/home/user/test_file.pptx"
  229. self.prepare_pptx(filename)
  230. self.open_file_and_check_viewer(filename, [],
  231. ["libreoffice"])
  232. def test_004_png(self):
  233. filename = "/home/user/test_file.png"
  234. self.prepare_png(filename)
  235. self.open_file_and_check_viewer(filename, [],
  236. ["shotwell", "eog", "display"])
  237. def test_005_jpg(self):
  238. filename = "/home/user/test_file.jpg"
  239. self.prepare_jpg(filename)
  240. self.open_file_and_check_viewer(filename, [],
  241. ["shotwell", "eog", "display"])
  242. def test_006_jpeg(self):
  243. filename = "/home/user/test_file.jpeg"
  244. self.prepare_jpg(filename)
  245. self.open_file_and_check_viewer(filename, [],
  246. ["shotwell", "eog", "display"])
  247. def test_010_url(self):
  248. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  249. ["Firefox", "Iceweasel", "Navigator"])
  250. def test_100_txt_dispvm(self):
  251. filename = "/home/user/test_file.txt"
  252. self.prepare_txt(filename)
  253. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  254. ["gedit", "emacs", "libreoffice"],
  255. dispvm=True)
  256. def test_101_pdf_dispvm(self):
  257. filename = "/home/user/test_file.pdf"
  258. self.prepare_pdf(filename)
  259. self.open_file_and_check_viewer(filename, [],
  260. ["evince"],
  261. dispvm=True)
  262. def test_102_doc_dispvm(self):
  263. filename = "/home/user/test_file.doc"
  264. self.prepare_doc(filename)
  265. self.open_file_and_check_viewer(filename, [],
  266. ["libreoffice", "abiword"],
  267. dispvm=True)
  268. def test_103_pptx_dispvm(self):
  269. filename = "/home/user/test_file.pptx"
  270. self.prepare_pptx(filename)
  271. self.open_file_and_check_viewer(filename, [],
  272. ["libreoffice"],
  273. dispvm=True)
  274. def test_104_png_dispvm(self):
  275. filename = "/home/user/test_file.png"
  276. self.prepare_png(filename)
  277. self.open_file_and_check_viewer(filename, [],
  278. ["shotwell", "eog", "display"],
  279. dispvm=True)
  280. def test_105_jpg_dispvm(self):
  281. filename = "/home/user/test_file.jpg"
  282. self.prepare_jpg(filename)
  283. self.open_file_and_check_viewer(filename, [],
  284. ["shotwell", "eog", "display"],
  285. dispvm=True)
  286. def test_106_jpeg_dispvm(self):
  287. filename = "/home/user/test_file.jpeg"
  288. self.prepare_jpg(filename)
  289. self.open_file_and_check_viewer(filename, [],
  290. ["shotwell", "eog", "display"],
  291. dispvm=True)
  292. def test_110_url_dispvm(self):
  293. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  294. ["Firefox", "Iceweasel", "Navigator"],
  295. dispvm=True)
  296. def load_tests(loader, tests, pattern):
  297. try:
  298. qc = qubes.qubes.QubesVmCollection()
  299. qc.lock_db_for_reading()
  300. qc.load()
  301. qc.unlock_db()
  302. templates = [vm.name for vm in qc.values() if
  303. isinstance(vm, qubes.qubes.QubesTemplateVm)]
  304. except OSError:
  305. templates = []
  306. for template in templates:
  307. tests.addTests(loader.loadTestsFromTestCase(
  308. type(
  309. 'TC_50_MimeHandlers_' + template,
  310. (TC_50_MimeHandlers, qubes.tests.QubesTestCase),
  311. {'template': template})))
  312. return tests