From 7038462e017cf6ce877c5a8de91290fbd563b93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 17 Apr 2019 13:32:32 +0200 Subject: [PATCH] daemon: Expose daemon users and user devices over a dbus interface. --- pantalaimon/daemon.py | 97 +++++++++++++++++++++++++++++++++++++++++-- pantalaimon/store.py | 52 ++++++++++++++++++++++- setup.py | 1 + 3 files changed, 145 insertions(+), 5 deletions(-) diff --git a/pantalaimon/daemon.py b/pantalaimon/daemon.py index 677d739..7c04b54 100755 --- a/pantalaimon/daemon.py +++ b/pantalaimon/daemon.py @@ -4,18 +4,27 @@ import asyncio import json import os import sys +from enum import Enum, auto +from functools import partial from ipaddress import ip_address from json import JSONDecodeError +from queue import Empty from urllib.parse import urlparse import aiohttp import attr import click +import dbus +import dbus.exceptions +import dbus.service +import janus import keyring import logbook from aiohttp import ClientSession, web from aiohttp.client_exceptions import ContentTypeError from appdirs import user_data_dir +from dbus.mainloop.glib import DBusGMainLoop +from gi.repository import GLib from logbook import StderrHandler from multidict import CIMultiDict from nio import EncryptionError, GroupEncryptionError, LoginResponse @@ -25,10 +34,74 @@ from pantalaimon.log import logger from pantalaimon.store import ClientInfo, PanStore +class Tasks(Enum): + shutdown = auto() + + +class Devices(dbus.service.Object): + def __init__(self, bus_name, device_list): + super().__init__(bus_name, "/org/pantalaimon/Devices") + self.device_list = device_list + + @dbus.service.method("org.pantalaimon.devices.list", + out_signature="a{sa{saa{ss}}}") + def list(self): + return self.device_list + + +class Users(dbus.service.Object): + def __init__(self, bus_name, user_list=None): + super().__init__(bus_name, "/org/pantalaimon/Users") + self.users = user_list + + @dbus.service.method("org.pantalaimon.users.list", + out_signature="a(ss)") + def list(self): + return self.users + + +def dbus_loop(task_queue, data_dir): + DBusGMainLoop(set_as_default=True) + loop = GLib.MainLoop() + + bus_name = dbus.service.BusName("org.pantalaimon", + bus=dbus.SessionBus(), + do_not_queue=True) + + store = PanStore(data_dir) + users = store.load_all_users() + devices = store.load_all_devices() + + # TODO update bus data if the asyncio thread tells us so. + Users(bus_name, users) + Devices(bus_name, devices) + + def task_callback(): + try: + task = task_queue.get_nowait() + except Empty: + return True + + if task == Tasks.shutdown: + task_queue.task_done() + loop.quit() + return False + + GLib.timeout_add(100, task_callback) + + loop.run() + + +async def shutdown_dbus(future, queue, app): + await queue.put(Tasks.shutdown) + await future + + @attr.s class ProxyDaemon: homeserver = attr.ib() data_dir = attr.ib() + queue = attr.ib() proxy = attr.ib(default=None) ssl = attr.ib(default=None) @@ -420,7 +493,7 @@ class ProxyDaemon: self.default_session = None -async def init(homeserver, http_proxy, ssl): +async def init(homeserver, http_proxy, ssl, queue): """Initialize the proxy and the http server.""" data_dir = user_data_dir("pantalaimon", "") @@ -429,7 +502,13 @@ async def init(homeserver, http_proxy, ssl): except OSError: pass - proxy = ProxyDaemon(homeserver, data_dir, proxy=http_proxy, ssl=ssl) + proxy = ProxyDaemon( + homeserver, + data_dir, + queue=queue, + proxy=http_proxy, + ssl=ssl, + ) app = web.Application() app.add_routes([ @@ -626,12 +705,24 @@ def start( logger.level = logbook.DEBUG loop = asyncio.get_event_loop() + + queue = janus.Queue(loop=loop) + proxy, app = loop.run_until_complete(init( homeserver, proxy.geturl() if proxy else None, - ssl + ssl, + queue.async_q )) + data_dir = user_data_dir("pantalaimon", "") + fut = loop.run_in_executor(None, dbus_loop, queue.sync_q, data_dir) + + kill_dbus_loop = partial(shutdown_dbus, fut, queue.async_q) + + app.on_shutdown.append(proxy.shutdown) + app.on_shutdown.append(kill_dbus_loop) + web.run_app(app, host=str(listen_address), port=listen_port) diff --git a/pantalaimon/store.py b/pantalaimon/store.py index 6d67ce7..5d44e50 100644 --- a/pantalaimon/store.py +++ b/pantalaimon/store.py @@ -1,8 +1,10 @@ import os +from collections import defaultdict from typing import Dict, List, Optional, Tuple import attr -from nio.store import Accounts, use_database +from nio.store import (Accounts, DeviceKeys, DeviceTrustState, TrustState, + use_database) from peewee import (SQL, DoesNotExist, ForeignKeyField, Model, SqliteDatabase, TextField) @@ -57,13 +59,30 @@ class ClientInfo: access_token = attr.ib(type=str) +@attr.s +class OlmDevice: + user_id = attr.ib() + id = attr.ib() + fp_key = attr.ib() + sender_key = attr.ib() + trust_state = attr.ib() + + @attr.s class PanStore: store_path = attr.ib(type=str) database_name = attr.ib(type=str, default="pan.db") database = attr.ib(type=SqliteDatabase, init=False) database_path = attr.ib(type=str, init=False) - models = [Accounts, AccessTokens, Clients, Servers, ServerUsers] + models = [ + Accounts, + AccessTokens, + Clients, + Servers, + ServerUsers, + DeviceKeys, + DeviceTrustState, + ] def __attrs_post_init__(self): self.database_path = os.path.join( @@ -191,3 +210,32 @@ class PanStore: clients[c.token] = client return clients + + @use_database + def load_all_devices(self): + # type (str, str) -> Dict[str, Dict[str, DeviceStore]] + store = defaultdict(dict) + + query = Accounts.select() + + for account in query: + device_store = [] + + for d in account.device_keys: + + try: + trust_state = d.trust_state[0].state + except IndexError: + trust_state = TrustState.unset + + device_store.append({ + "user_id": d.user_id, + "device_id": d.device_id, + "fingerprint_key": d.fp_key, + "sender_key": d.sender_key, + "trust_state": trust_state.name + }) + + store[account.user_id][account.device_id] = device_store + + return store diff --git a/setup.py b/setup.py index c9441f7..90b4620 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ setup( "keyring", "logbook", "peewee", + "dbus-python", "typing;python_version<'3.5'", "matrix-nio" ],