__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. # Copyright (C) 2017 Marek Marczykowski-Górecki
  7. # <marmarek@invisiblethingslab.com>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (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 along
  20. # with this program; if not, see <http://www.gnu.org/licenses/>.
  21. import asyncio
  22. import errno
  23. import functools
  24. import io
  25. import os
  26. import shutil
  27. import socket
  28. import struct
  29. import traceback
  30. import qubes.exc
  31. class ProtocolError(AssertionError):
  32. '''Raised when something is wrong with data received'''
  33. pass
  34. class PermissionDenied(Exception):
  35. '''Raised deliberately by handlers when we decide not to cooperate'''
  36. pass
  37. def method(name, *, no_payload=False, endpoints=None):
  38. '''Decorator factory for methods intended to appear in API.
  39. The decorated method can be called from public API using a child of
  40. :py:class:`AbstractQubesMgmt` class. The method becomes "public", and can be
  41. called using remote management interface.
  42. :param str name: qrexec rpc method name
  43. :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
  44. also will not pass payload at all to the method
  45. The expected function method should have one argument (other than usual
  46. *self*), ``untrusted_payload``, which will contain the payload.
  47. .. warning::
  48. This argument has to be named such, to remind the programmer that the
  49. content of this variable is indeed untrusted.
  50. If *no_payload* is true, then the method is called with no arguments.
  51. '''
  52. def decorator(func):
  53. if no_payload:
  54. # the following assignment is needed for how closures work in Python
  55. _func = func
  56. @functools.wraps(_func)
  57. def wrapper(self, untrusted_payload, **kwargs):
  58. if untrusted_payload != b'':
  59. raise ProtocolError('unexpected payload')
  60. return _func(self, **kwargs)
  61. func = wrapper
  62. # pylint: disable=protected-access
  63. if endpoints is None:
  64. func._rpcname = ((name, None),)
  65. else:
  66. func._rpcname = tuple(
  67. (name.format(endpoint=endpoint), endpoint)
  68. for endpoint in endpoints)
  69. return func
  70. return decorator
  71. def apply_filters(iterable, filters):
  72. '''Apply filters returned by mgmt-permission:... event'''
  73. for selector in filters:
  74. iterable = filter(selector, iterable)
  75. return iterable
  76. class AbstractQubesAPI(object):
  77. '''Common code for Qubes Management Protocol handling
  78. Different interfaces can expose different API call sets, however they share
  79. common protocol and common implementation framework. This class is the
  80. latter.
  81. To implement a new interface, inherit from this class and write at least one
  82. method and decorate it with :py:func:`api` decorator. It will have access to
  83. pre-defined attributes: :py:attr:`app`, :py:attr:`src`, :py:attr:`dest`,
  84. :py:attr:`arg` and :py:attr:`method`.
  85. There are also two helper functions for firing events associated with API
  86. calls.
  87. '''
  88. #: the preferred socket location (to be overridden in child's class)
  89. SOCKNAME = None
  90. def __init__(self, app, src, method_name, dest, arg, send_event=None):
  91. #: :py:class:`qubes.Qubes` object
  92. self.app = app
  93. #: source qube
  94. self.src = self.app.domains[src.decode('ascii')]
  95. #: destination qube
  96. self.dest = self.app.domains[dest.decode('ascii')]
  97. #: argument
  98. self.arg = arg.decode('ascii')
  99. #: name of the method
  100. self.method = method_name.decode('ascii')
  101. #: callback for sending events if applicable
  102. self.send_event = send_event
  103. #: is this operation cancellable?
  104. self.cancellable = False
  105. untrusted_candidates = []
  106. for attr in dir(self):
  107. func = getattr(self, attr)
  108. if not callable(func):
  109. continue
  110. try:
  111. # pylint: disable=protected-access
  112. for mname, endpoint in func._rpcname:
  113. if mname != self.method:
  114. continue
  115. untrusted_candidates.append((func, endpoint))
  116. except AttributeError:
  117. continue
  118. if not untrusted_candidates:
  119. raise ProtocolError('no such method: {!r}'.format(self.method))
  120. assert len(untrusted_candidates) == 1, \
  121. 'multiple candidates for method {!r}'.format(self.method)
  122. #: the method to execute
  123. self._handler = untrusted_candidates[0]
  124. self._running_handler = None
  125. del untrusted_candidates
  126. def execute(self, *, untrusted_payload):
  127. '''Execute management operation.
  128. This method is a coroutine.
  129. '''
  130. handler, endpoint = self._handler
  131. kwargs = {}
  132. if endpoint is not None:
  133. kwargs['endpoint'] = endpoint
  134. self._running_handler = asyncio.ensure_future(handler(
  135. untrusted_payload=untrusted_payload, **kwargs))
  136. return self._running_handler
  137. def cancel(self):
  138. '''If operation is cancellable, interrupt it'''
  139. if self.cancellable and self._running_handler is not None:
  140. self._running_handler.cancel()
  141. def fire_event_for_permission(self, **kwargs):
  142. '''Fire an event on the source qube to check for permission'''
  143. return self.src.fire_event('mgmt-permission:' + self.method,
  144. pre_event=True, dest=self.dest, arg=self.arg, **kwargs)
  145. def fire_event_for_filter(self, iterable, **kwargs):
  146. '''Fire an event on the source qube to filter for permission'''
  147. return apply_filters(iterable,
  148. self.fire_event_for_permission(**kwargs))
  149. class QubesDaemonProtocol(asyncio.Protocol):
  150. buffer_size = 65536
  151. header = struct.Struct('Bx')
  152. def __init__(self, handler, *args, app, debug=False, **kwargs):
  153. super().__init__(*args, **kwargs)
  154. self.handler = handler
  155. self.app = app
  156. self.untrusted_buffer = io.BytesIO()
  157. self.len_untrusted_buffer = 0
  158. self.transport = None
  159. self.debug = debug
  160. self.event_sent = False
  161. self.mgmt = None
  162. def connection_made(self, transport):
  163. self.transport = transport
  164. def connection_lost(self, exc):
  165. self.untrusted_buffer.close()
  166. # for cancellable operation, interrupt it, otherwise it will do nothing
  167. if self.mgmt is not None:
  168. self.mgmt.cancel()
  169. self.transport = None
  170. def data_received(self, untrusted_data): # pylint: disable=arguments-differ
  171. if self.len_untrusted_buffer + len(untrusted_data) > self.buffer_size:
  172. self.app.log.warning('request too long')
  173. self.transport.abort()
  174. self.untrusted_buffer.close()
  175. return
  176. self.len_untrusted_buffer += \
  177. self.untrusted_buffer.write(untrusted_data)
  178. def eof_received(self):
  179. try:
  180. src, meth, dest, arg, untrusted_payload = \
  181. self.untrusted_buffer.getvalue().split(b'\0', 4)
  182. except ValueError:
  183. self.app.log.warning('framing error')
  184. self.transport.abort()
  185. return
  186. finally:
  187. self.untrusted_buffer.close()
  188. asyncio.ensure_future(self.respond(
  189. src, meth, dest, arg, untrusted_payload=untrusted_payload))
  190. return True
  191. @asyncio.coroutine
  192. def respond(self, src, meth, dest, arg, *, untrusted_payload):
  193. try:
  194. self.mgmt = self.handler(self.app, src, meth, dest, arg,
  195. self.send_event)
  196. response = yield from self.mgmt.execute(
  197. untrusted_payload=untrusted_payload)
  198. assert not (self.event_sent and response)
  199. if self.transport is None:
  200. return
  201. # except clauses will fall through to transport.abort() below
  202. except PermissionDenied:
  203. self.app.log.warning(
  204. 'permission denied for call %s+%s (%s → %s) '
  205. 'with payload of %d bytes',
  206. meth, arg, src, dest, len(untrusted_payload))
  207. except ProtocolError:
  208. self.app.log.warning(
  209. 'protocol error for call %s+%s (%s → %s) '
  210. 'with payload of %d bytes',
  211. meth, arg, src, dest, len(untrusted_payload))
  212. except qubes.exc.QubesException as err:
  213. msg = ('%r while calling '
  214. 'src=%r meth=%r dest=%r arg=%r len(untrusted_payload)=%d')
  215. if self.debug:
  216. self.app.log.exception(msg,
  217. err, src, meth, dest, arg, len(untrusted_payload))
  218. else:
  219. self.app.log.info(msg,
  220. err, src, meth, dest, arg, len(untrusted_payload))
  221. if self.transport is not None:
  222. self.send_exception(err)
  223. self.transport.write_eof()
  224. self.transport.close()
  225. return
  226. except Exception: # pylint: disable=broad-except
  227. self.app.log.exception(
  228. 'unhandled exception while calling '
  229. 'src=%r meth=%r dest=%r arg=%r len(untrusted_payload)=%d',
  230. src, meth, dest, arg, len(untrusted_payload))
  231. else:
  232. if not self.event_sent:
  233. self.send_response(response)
  234. try:
  235. self.transport.write_eof()
  236. except NotImplementedError:
  237. pass
  238. self.transport.close()
  239. return
  240. # this is reached if from except: blocks; do not put it in finally:,
  241. # because this will prevent the good case from sending the reply
  242. self.transport.abort()
  243. def send_header(self, *args):
  244. self.transport.write(self.header.pack(*args))
  245. def send_response(self, content):
  246. assert not self.event_sent
  247. self.send_header(0x30)
  248. if content is not None:
  249. self.transport.write(content.encode('utf-8'))
  250. def send_event(self, subject, event, **kwargs):
  251. self.event_sent = True
  252. self.send_header(0x31)
  253. if subject is not self.app:
  254. self.transport.write(subject.name.encode('ascii'))
  255. self.transport.write(b'\0')
  256. self.transport.write(event.encode('ascii') + b'\0')
  257. for k, v in kwargs.items():
  258. self.transport.write('{}\0{}\0'.format(k, str(v)).encode('ascii'))
  259. self.transport.write(b'\0')
  260. def send_exception(self, exc):
  261. self.send_header(0x32)
  262. self.transport.write(type(exc).__name__.encode() + b'\0')
  263. if self.debug:
  264. self.transport.write(''.join(traceback.format_exception(
  265. type(exc), exc, exc.__traceback__)).encode('utf-8'))
  266. self.transport.write(b'\0')
  267. self.transport.write(str(exc).encode('utf-8') + b'\0')
  268. @asyncio.coroutine
  269. def create_servers(*args, force=False, loop=None, **kwargs):
  270. '''Create multiple Qubes API servers
  271. :param qubes.Qubes app: the app that is a backend of the servers
  272. :param bool force: if :py:obj:`True`, unconditionaly remove existing \
  273. sockets; if :py:obj:`False`, raise an error if there is some process \
  274. listening to such socket
  275. :param asyncio.Loop loop: loop
  276. *args* are supposed to be classess inheriting from
  277. :py:class:`AbstractQubesAPI`
  278. *kwargs* (like *app* or *debug* for example) are passed to
  279. :py:class:`QubesDaemonProtocol` constructor
  280. '''
  281. loop = loop or asyncio.get_event_loop()
  282. servers = []
  283. old_umask = os.umask(0o007)
  284. try:
  285. # XXX this can be optimised with asyncio.wait() to start servers in
  286. # parallel, but I currently don't see the need
  287. for handler in args:
  288. sockpath = handler.SOCKNAME
  289. assert sockpath is not None, \
  290. 'SOCKNAME needs to be overloaded in {}'.format(
  291. type(handler).__name__)
  292. if os.path.exists(sockpath):
  293. if force:
  294. os.unlink(sockpath)
  295. else:
  296. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  297. try:
  298. sock.connect(sockpath)
  299. except ConnectionRefusedError:
  300. # dead socket, remove it anyway
  301. os.unlink(sockpath)
  302. else:
  303. # woops, someone is listening
  304. sock.close()
  305. raise FileExistsError(errno.EEXIST,
  306. 'socket already exists: {!r}'.format(sockpath))
  307. server = yield from loop.create_unix_server(
  308. functools.partial(QubesDaemonProtocol, handler, **kwargs),
  309. sockpath)
  310. for sock in server.sockets:
  311. shutil.chown(sock.getsockname(), group='qubes')
  312. servers.append(server)
  313. finally:
  314. os.umask(old_umask)
  315. return servers