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. import os
  24. from distutils import spawn
  25. import re
  26. import subprocess
  27. import time
  28. import unittest
  29. import itertools
  30. import asyncio
  31. import sys
  32. import qubes.tests
  33. import qubes
  34. @unittest.skipUnless(
  35. spawn.find_executable('xprop') and
  36. spawn.find_executable('xdotool') and
  37. spawn.find_executable('wmctrl'),
  38. "xprop or xdotool or wmctrl not installed")
  39. class TC_50_MimeHandlers:
  40. def setUp(self):
  41. super(TC_50_MimeHandlers, self).setUp()
  42. if self.template.startswith('whonix-gw') or 'minimal' in self.template:
  43. raise unittest.SkipTest(
  44. 'Template {} not supported by this test'.format(self.template))
  45. self.source_vmname = self.make_vm_name('source')
  46. self.source_vm = self.app.add_new_vm("AppVM",
  47. template=self.template,
  48. name=self.source_vmname,
  49. label='red')
  50. self.loop.run_until_complete(self.source_vm.create_on_disk())
  51. self.target_vmname = self.make_vm_name('target')
  52. self.target_vm = self.app.add_new_vm("AppVM",
  53. template=self.template,
  54. name=self.target_vmname,
  55. label='red')
  56. self.loop.run_until_complete(self.target_vm.create_on_disk())
  57. self.target_vm.template_for_dispvms = True
  58. self.source_vm.default_dispvm = self.target_vm
  59. self.loop.run_until_complete(asyncio.gather(
  60. self.source_vm.start(),
  61. self.target_vm.start()))
  62. def get_window_class(self, winid, dispvm=False):
  63. (vm_winid, _) = subprocess.Popen(
  64. ['xprop', '-id', winid, '_QUBES_VMWINDOWID'],
  65. stdout=subprocess.PIPE
  66. ).communicate()
  67. vm_winid = vm_winid.decode().split("#")[1].strip('\n" ')
  68. if dispvm:
  69. (vmname, _) = subprocess.Popen(
  70. ['xprop', '-id', winid, '_QUBES_VMNAME'],
  71. stdout=subprocess.PIPE
  72. ).communicate()
  73. vmname = vmname.decode().split("=")[1].strip('\n" ')
  74. vm = self.app.domains[vmname]
  75. else:
  76. vm = self.target_vm
  77. window_class = None
  78. while window_class is None:
  79. try:
  80. window_class, _ = self.loop.run_until_complete(
  81. vm.run_for_stdio('xprop -id {} WM_CLASS'.format(vm_winid)))
  82. except subprocess.CalledProcessError as e:
  83. if e.returncode == 127:
  84. self.skipTest('xprop not installed')
  85. self.fail(
  86. "xprop -id {} WM_CLASS failed: {}".format(
  87. vm_winid, e.stderr.decode()))
  88. if b'not found' in window_class:
  89. # WM_CLASS not set yet, wait a little
  90. time.sleep(0.1)
  91. window_class = None
  92. # output: WM_CLASS(STRING) = "gnome-terminal-server", "Gnome-terminal"
  93. try:
  94. window_class = window_class.decode()
  95. window_class = window_class.split("=")[1].split(",")[0].strip('\n" ')
  96. except IndexError:
  97. raise Exception(
  98. "Unexpected output from xprop: '{}'".format(window_class))
  99. return window_class
  100. def open_file_and_check_viewer(self, filename, expected_app_titles,
  101. expected_app_classes, dispvm=False):
  102. if dispvm:
  103. p = self.loop.run_until_complete(self.source_vm.run(
  104. "qvm-open-in-dvm {}".format(filename), stdout=subprocess.PIPE))
  105. vmpattern = "disp[0-9]*"
  106. else:
  107. p = self.loop.run_until_complete(self.source_vm.run(
  108. "qvm-open-in-vm {} {}".format(self.target_vmname, filename),
  109. stdout=subprocess.PIPE))
  110. vmpattern = self.target_vmname
  111. wait_count = 0
  112. winid = None
  113. with self.qrexec_policy('qubes.OpenInVM', self.source_vm.name,
  114. self.target_vmname):
  115. with self.qrexec_policy('qubes.OpenURL', self.source_vm.name,
  116. self.target_vmname):
  117. while True:
  118. search = subprocess.Popen(['xdotool', 'search',
  119. '--onlyvisible', '--class', vmpattern],
  120. stdout=subprocess.PIPE,
  121. stderr=subprocess.DEVNULL)
  122. retcode = search.wait()
  123. if retcode == 0:
  124. winid = search.stdout.read().strip()
  125. # get window title
  126. (window_title, _) = subprocess.Popen(
  127. ['xdotool', 'getwindowname', winid], stdout=subprocess.PIPE). \
  128. communicate()
  129. window_title = window_title.decode('utf8').strip()
  130. # ignore LibreOffice splash screen and window with no title
  131. # set yet
  132. if window_title and \
  133. not window_title.startswith("LibreOffice") and\
  134. not window_title.startswith("NetworkManager") and\
  135. not window_title == 'VMapp command':
  136. break
  137. wait_count += 1
  138. if wait_count > 100:
  139. self.fail("Timeout while waiting for editor window")
  140. self.loop.run_until_complete(asyncio.sleep(0.3))
  141. # get window class
  142. window_class = self.get_window_class(winid, dispvm)
  143. # close the window - we've got the window class, it is no longer needed
  144. subprocess.check_call(['wmctrl', '-i', '-c', winid])
  145. # confirm quit for example for Firefox
  146. self.loop.run_until_complete(asyncio.sleep(1))
  147. subprocess.call(['xdotool', 'search', '--onlyvisible', '--name',
  148. 'Quit', 'windowfocus', 'key', 'Return'])
  149. try:
  150. self.loop.run_until_complete(asyncio.wait_for(p.wait(), 30))
  151. except asyncio.TimeoutError:
  152. self.fail('qvm-open-in-vm did not exited')
  153. self.wait_for_window(window_title, show=False)
  154. def check_matches(obj, patterns):
  155. return any((pat.search(obj) if isinstance(pat, type(re.compile('')))
  156. else pat in obj) for pat in patterns)
  157. if not check_matches(window_title, expected_app_titles) and \
  158. not check_matches(window_class, expected_app_classes):
  159. self.fail("Opening file {} resulted in window '{} ({})', which is "
  160. "none of {!r} ({!r})".format(
  161. filename, window_title, window_class,
  162. expected_app_titles, expected_app_classes))
  163. def prepare_txt(self, filename):
  164. self.loop.run_until_complete(
  165. self.source_vm.run_for_stdio("cat > {}".format(filename),
  166. input=b'This is test\n'))
  167. def prepare_pdf(self, filename):
  168. self.prepare_txt("/tmp/source.txt")
  169. cmd = "convert text:/tmp/source.txt {}".format(filename)
  170. try:
  171. self.loop.run_until_complete(
  172. self.source_vm.run_for_stdio(cmd))
  173. except subprocess.CalledProcessError as e:
  174. self.fail('{} failed: {}'.format(cmd, e.stderr.decode()))
  175. def prepare_doc(self, filename):
  176. self.prepare_txt("/tmp/source.txt")
  177. cmd = "unoconv -f doc -o {} /tmp/source.txt".format(filename)
  178. try:
  179. self.loop.run_until_complete(
  180. self.source_vm.run_for_stdio(cmd))
  181. except subprocess.CalledProcessError as e:
  182. if e.returncode == 127:
  183. self.skipTest("unoconv not installed".format(cmd))
  184. self.skipTest("Failed to run '{}': {}".format(cmd,
  185. e.stderr.decode()))
  186. def prepare_pptx(self, filename):
  187. self.prepare_txt("/tmp/source.txt")
  188. cmd = "unoconv -f pptx -o {} /tmp/source.txt".format(filename)
  189. try:
  190. self.loop.run_until_complete(
  191. self.source_vm.run_for_stdio(cmd))
  192. except subprocess.CalledProcessError as e:
  193. if e.returncode == 127:
  194. self.skipTest("unoconv not installed".format(cmd))
  195. self.skipTest("Failed to run '{}': {}".format(cmd,
  196. e.stderr.decode()))
  197. def prepare_png(self, filename):
  198. self.prepare_txt("/tmp/source.txt")
  199. cmd = "convert text:/tmp/source.txt {}".format(filename)
  200. try:
  201. self.loop.run_until_complete(
  202. self.source_vm.run_for_stdio(cmd))
  203. except subprocess.CalledProcessError as e:
  204. if e.returncode == 127:
  205. self.skipTest("convert not installed".format(cmd))
  206. self.skipTest("Failed to run '{}': {}".format(cmd,
  207. e.stderr.decode()))
  208. def prepare_jpg(self, filename):
  209. self.prepare_txt("/tmp/source.txt")
  210. cmd = "convert text:/tmp/source.txt {}".format(filename)
  211. try:
  212. self.loop.run_until_complete(
  213. self.source_vm.run_for_stdio(cmd))
  214. except subprocess.CalledProcessError as e:
  215. if e.returncode == 127:
  216. self.skipTest("convert not installed".format(cmd))
  217. self.skipTest("Failed to run '{}': {}".format(cmd,
  218. e.stderr.decode()))
  219. def test_000_txt(self):
  220. filename = "/home/user/test_file.txt"
  221. self.prepare_txt(filename)
  222. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  223. ["gedit", "emacs", "libreoffice"])
  224. def test_001_pdf(self):
  225. filename = "/home/user/test_file.pdf"
  226. self.prepare_pdf(filename)
  227. self.open_file_and_check_viewer(filename, [],
  228. ["evince"])
  229. def test_002_doc(self):
  230. filename = "/home/user/test_file.doc"
  231. self.prepare_doc(filename)
  232. self.open_file_and_check_viewer(filename, [],
  233. ["libreoffice", "abiword"])
  234. def test_003_pptx(self):
  235. filename = "/home/user/test_file.pptx"
  236. self.prepare_pptx(filename)
  237. self.open_file_and_check_viewer(filename, [],
  238. ["libreoffice"])
  239. def test_004_png(self):
  240. filename = "/home/user/test_file.png"
  241. self.prepare_png(filename)
  242. self.open_file_and_check_viewer(filename, [],
  243. ["shotwell", "eog", "display"])
  244. def test_005_jpg(self):
  245. filename = "/home/user/test_file.jpg"
  246. self.prepare_jpg(filename)
  247. self.open_file_and_check_viewer(filename, [],
  248. ["shotwell", "eog", "display"])
  249. def test_006_jpeg(self):
  250. filename = "/home/user/test_file.jpeg"
  251. self.prepare_jpg(filename)
  252. self.open_file_and_check_viewer(filename, [],
  253. ["shotwell", "eog", "display"])
  254. def test_010_url(self):
  255. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  256. ["Firefox", "Iceweasel", "Navigator"])
  257. def test_100_txt_dispvm(self):
  258. filename = "/home/user/test_file.txt"
  259. self.prepare_txt(filename)
  260. self.open_file_and_check_viewer(filename, ["vim", "user@"],
  261. ["gedit", "emacs", "libreoffice"],
  262. dispvm=True)
  263. def test_101_pdf_dispvm(self):
  264. filename = "/home/user/test_file.pdf"
  265. self.prepare_pdf(filename)
  266. self.open_file_and_check_viewer(filename, [],
  267. ["evince"],
  268. dispvm=True)
  269. def test_102_doc_dispvm(self):
  270. filename = "/home/user/test_file.doc"
  271. self.prepare_doc(filename)
  272. self.open_file_and_check_viewer(filename, [],
  273. ["libreoffice", "abiword"],
  274. dispvm=True)
  275. def test_103_pptx_dispvm(self):
  276. filename = "/home/user/test_file.pptx"
  277. self.prepare_pptx(filename)
  278. self.open_file_and_check_viewer(filename, [],
  279. ["libreoffice"],
  280. dispvm=True)
  281. def test_104_png_dispvm(self):
  282. filename = "/home/user/test_file.png"
  283. self.prepare_png(filename)
  284. self.open_file_and_check_viewer(filename, [],
  285. ["shotwell", "eog", "display"],
  286. dispvm=True)
  287. def test_105_jpg_dispvm(self):
  288. filename = "/home/user/test_file.jpg"
  289. self.prepare_jpg(filename)
  290. self.open_file_and_check_viewer(filename, [],
  291. ["shotwell", "eog", "display"],
  292. dispvm=True)
  293. def test_106_jpeg_dispvm(self):
  294. filename = "/home/user/test_file.jpeg"
  295. self.prepare_jpg(filename)
  296. self.open_file_and_check_viewer(filename, [],
  297. ["shotwell", "eog", "display"],
  298. dispvm=True)
  299. def test_110_url_dispvm(self):
  300. self.open_file_and_check_viewer("https://www.qubes-os.org/", [],
  301. ["Firefox", "Iceweasel", "Navigator"],
  302. dispvm=True)
  303. def create_testcases_for_templates():
  304. return qubes.tests.create_testcases_for_templates('TC_50_MimeHandlers',
  305. TC_50_MimeHandlers, qubes.tests.SystemTestCase,
  306. module=sys.modules[__name__])
  307. def load_tests(loader, tests, pattern):
  308. tests.addTests(loader.loadTestsFromNames(
  309. create_testcases_for_templates()))
  310. return tests
  311. qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)