mime.py 14 KB

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