2019-04-04 19:39:44 +02:00
|
|
|
import asyncio
|
2019-04-04 11:30:42 +02:00
|
|
|
from pprint import pformat
|
2019-04-12 14:19:37 +02:00
|
|
|
from typing import Any, Dict, Optional
|
2019-04-03 16:48:13 +02:00
|
|
|
|
2019-04-28 21:13:19 +02:00
|
|
|
from aiohttp.client_exceptions import (ClientProxyConnectionError,
|
|
|
|
ServerDisconnectedError)
|
2019-04-11 14:22:55 +02:00
|
|
|
from nio import (AsyncClient, ClientConfig, EncryptionError,
|
|
|
|
GroupEncryptionError, KeysQueryResponse, MegolmEvent,
|
2019-04-28 21:17:10 +02:00
|
|
|
RoomEncryptedEvent, SyncResponse,
|
|
|
|
KeyVerificationEvent, LocalProtocolError,
|
|
|
|
KeyVerificationStart, KeyVerificationKey, KeyVerificationMac)
|
2019-04-11 14:22:55 +02:00
|
|
|
from nio.store import SqliteStore
|
2019-04-03 16:48:13 +02:00
|
|
|
|
2019-04-04 11:30:42 +02:00
|
|
|
from pantalaimon.log import logger
|
2019-04-18 11:43:07 +02:00
|
|
|
from pantalaimon.ui import DevicesMessage
|
2019-04-04 11:30:42 +02:00
|
|
|
|
2019-04-03 16:48:13 +02:00
|
|
|
|
2019-04-11 14:22:55 +02:00
|
|
|
class PanClient(AsyncClient):
|
2019-04-03 16:48:13 +02:00
|
|
|
"""A wrapper class around a nio AsyncClient extending its functionality."""
|
|
|
|
|
2019-04-04 19:39:44 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
homeserver,
|
2019-04-18 11:43:07 +02:00
|
|
|
queue=None,
|
2019-04-04 19:39:44 +02:00
|
|
|
user="",
|
|
|
|
device_id="",
|
|
|
|
store_path="",
|
|
|
|
config=None,
|
|
|
|
ssl=None,
|
|
|
|
proxy=None
|
|
|
|
):
|
2019-04-11 14:21:39 +02:00
|
|
|
config = config or ClientConfig(store=SqliteStore, store_name="pan.db")
|
2019-04-04 19:39:44 +02:00
|
|
|
super().__init__(homeserver, user, device_id, store_path, config,
|
|
|
|
ssl, proxy)
|
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
self.task = None
|
2019-04-18 11:43:07 +02:00
|
|
|
self.queue = queue
|
2019-04-04 19:39:44 +02:00
|
|
|
self.loop_stopped = asyncio.Event()
|
|
|
|
self.synced = asyncio.Event()
|
|
|
|
|
2019-04-28 21:17:10 +02:00
|
|
|
self.add_to_device_callback(
|
|
|
|
self.key_verification_cb,
|
|
|
|
KeyVerificationEvent
|
|
|
|
)
|
2019-05-02 12:09:49 +02:00
|
|
|
self.add_event_callback(
|
|
|
|
self.undecrypted_event_cb,
|
|
|
|
MegolmEvent
|
|
|
|
)
|
2019-04-29 09:49:42 +02:00
|
|
|
self.key_verificatins_tasks = []
|
2019-05-02 12:09:49 +02:00
|
|
|
self.key_request_tasks = []
|
2019-04-28 21:17:10 +02:00
|
|
|
|
2019-04-04 19:39:44 +02:00
|
|
|
def verify_devices(self, changed_devices):
|
|
|
|
# Verify new devices automatically for now.
|
|
|
|
for user_id, device_dict in changed_devices.items():
|
|
|
|
for device in device_dict.values():
|
|
|
|
if device.deleted:
|
|
|
|
continue
|
|
|
|
|
|
|
|
logger.info("Automatically verifying device {} of "
|
|
|
|
"user {}".format(device.id, user_id))
|
|
|
|
self.verify_device(device)
|
|
|
|
|
2019-05-02 12:09:49 +02:00
|
|
|
def undecrypted_event_cb(self, room, event):
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
|
|
|
logger.info("Unable to decrypt event from {} via {}.".format(
|
|
|
|
event.sender,
|
|
|
|
event.device_id
|
|
|
|
))
|
|
|
|
|
|
|
|
if event.session_id not in self.outgoing_key_requests:
|
|
|
|
logger.info("Requesting room key for undecrypted event.")
|
|
|
|
task = loop.create_task(self.request_room_key(event))
|
|
|
|
self.key_request_tasks.append(task)
|
|
|
|
|
2019-04-28 21:17:10 +02:00
|
|
|
def key_verification_cb(self, event):
|
|
|
|
logger.info("Received key verification event: {}".format(event))
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
|
|
|
if isinstance(event, KeyVerificationStart):
|
2019-04-29 09:49:42 +02:00
|
|
|
task = loop.create_task(
|
|
|
|
self.accept_key_verification(event.transaction_id)
|
|
|
|
)
|
|
|
|
self.key_verificatins_tasks.append(task)
|
2019-04-28 21:17:10 +02:00
|
|
|
|
|
|
|
elif isinstance(event, KeyVerificationKey):
|
|
|
|
sas = self.key_verifications.get(event.transaction_id, None)
|
|
|
|
if not sas:
|
|
|
|
return
|
|
|
|
|
|
|
|
emoji = sas.get_emoji()
|
|
|
|
|
|
|
|
emojies = [x[0] for x in emoji]
|
|
|
|
descriptions = [x[1] for x in emoji]
|
|
|
|
device = sas.other_olm_device
|
|
|
|
|
|
|
|
emoji_str = u"{:^10}{:^10}{:^10}{:^10}{:^10}{:^10}{:^10}".format(
|
|
|
|
*emojies
|
|
|
|
)
|
|
|
|
desc = u"{:^11}{:^11}{:^11}{:^11}{:^11}{:^11}{:^11}".format(
|
|
|
|
*descriptions
|
|
|
|
)
|
|
|
|
short_string = u"\n".join([emoji_str, desc])
|
|
|
|
|
|
|
|
logger.info(u"Short authentication string for {} via {}:\n"
|
|
|
|
u"{}".format(device.user_id, device.id, short_string))
|
|
|
|
|
|
|
|
elif isinstance(event, KeyVerificationMac):
|
2019-04-29 09:49:42 +02:00
|
|
|
task = loop.create_task(
|
|
|
|
self.accept_short_auth_string(event.transaction_id)
|
|
|
|
)
|
|
|
|
self.key_verificatins_tasks.append(task)
|
2019-04-28 21:17:10 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
def start_loop(self):
|
2019-04-04 19:39:44 +02:00
|
|
|
"""Start a loop that runs forever and keeps on syncing with the server.
|
|
|
|
|
|
|
|
The loop can be stopped with the stop_loop() method.
|
|
|
|
"""
|
2019-04-11 14:20:09 +02:00
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
task = loop.create_task(self.loop())
|
|
|
|
self.task = task
|
|
|
|
return task
|
|
|
|
|
2019-04-28 21:15:14 +02:00
|
|
|
async def _to_device(self, message):
|
|
|
|
response = await self.to_device(message)
|
|
|
|
return message, response
|
|
|
|
|
|
|
|
async def send_to_device_messages(self):
|
|
|
|
if not self.outgoing_to_device_messages:
|
|
|
|
return
|
|
|
|
|
|
|
|
tasks = []
|
|
|
|
|
|
|
|
for message in self.outgoing_to_device_messages:
|
|
|
|
task = asyncio.create_task(self._to_device(message))
|
|
|
|
tasks.append(task)
|
|
|
|
|
2019-05-02 12:09:49 +02:00
|
|
|
await asyncio.gather(*tasks)
|
2019-04-28 21:15:14 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
async def loop(self):
|
2019-04-04 19:39:44 +02:00
|
|
|
self.loop_running = True
|
|
|
|
self.loop_stopped.clear()
|
|
|
|
|
|
|
|
logger.info(f"Starting sync loop for {self.user_id}")
|
|
|
|
|
2019-04-17 13:31:37 +02:00
|
|
|
while True:
|
|
|
|
try:
|
2019-04-11 14:20:09 +02:00
|
|
|
if not self.logged_in:
|
|
|
|
# TODO login
|
|
|
|
pass
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-29 16:56:50 +02:00
|
|
|
response = await self.sync(
|
|
|
|
30000,
|
|
|
|
sync_filter={
|
|
|
|
"room": {
|
|
|
|
"state": {"lazy_load_members": True}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2019-04-17 13:31:37 +02:00
|
|
|
if response.transport_response.status != 200:
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
continue
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-28 21:15:14 +02:00
|
|
|
await self.send_to_device_messages()
|
|
|
|
|
2019-04-29 09:49:42 +02:00
|
|
|
try:
|
|
|
|
await asyncio.gather(*self.key_verificatins_tasks)
|
|
|
|
except LocalProtocolError as e:
|
|
|
|
logger.info(e)
|
|
|
|
|
|
|
|
self.key_verificatins_tasks = []
|
|
|
|
|
2019-05-02 12:09:49 +02:00
|
|
|
await asyncio.gather(*self.key_request_tasks)
|
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
if self.should_upload_keys:
|
|
|
|
await self.keys_upload()
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
if self.should_query_keys:
|
|
|
|
key_query_response = await self.keys_query()
|
|
|
|
if isinstance(key_query_response, KeysQueryResponse):
|
|
|
|
self.verify_devices(key_query_response.changed)
|
2019-04-18 11:43:07 +02:00
|
|
|
message = DevicesMessage(
|
|
|
|
self.user_id,
|
|
|
|
key_query_response.changed
|
|
|
|
)
|
|
|
|
await self.queue.put(message)
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
if not isinstance(response, SyncResponse):
|
|
|
|
# TODO error handling
|
|
|
|
pass
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
self.synced.set()
|
|
|
|
self.synced.clear()
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-17 13:31:37 +02:00
|
|
|
except asyncio.CancelledError:
|
|
|
|
logger.info("Stopping the sync loop")
|
|
|
|
self._loop_stop()
|
|
|
|
break
|
|
|
|
|
2019-04-28 21:13:19 +02:00
|
|
|
except (
|
|
|
|
ClientProxyConnectionError,
|
|
|
|
ServerDisconnectedError,
|
|
|
|
ConnectionRefusedError
|
|
|
|
):
|
2019-04-17 13:31:37 +02:00
|
|
|
try:
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
self._loop_stop()
|
|
|
|
break
|
|
|
|
|
|
|
|
def _loop_stop(self):
|
|
|
|
self.loop_running = False
|
|
|
|
self.loop_stopped.set()
|
2019-04-04 19:39:44 +02:00
|
|
|
|
|
|
|
async def loop_stop(self):
|
2019-04-11 14:20:09 +02:00
|
|
|
"""Stop the client loop."""
|
2019-04-17 13:31:55 +02:00
|
|
|
if not self.task or self.task.done():
|
2019-04-11 14:20:09 +02:00
|
|
|
return
|
2019-04-04 19:39:44 +02:00
|
|
|
|
2019-04-11 14:20:09 +02:00
|
|
|
self.task.cancel()
|
2019-04-04 19:39:44 +02:00
|
|
|
await self.loop_stopped.wait()
|
|
|
|
|
|
|
|
async def encrypt(self, room_id, msgtype, content):
|
|
|
|
try:
|
|
|
|
return super().encrypt(
|
|
|
|
room_id,
|
|
|
|
msgtype,
|
|
|
|
content
|
|
|
|
)
|
|
|
|
except GroupEncryptionError:
|
|
|
|
await self.share_group_session(room_id)
|
|
|
|
return super().encrypt(
|
|
|
|
room_id,
|
|
|
|
msgtype,
|
|
|
|
content
|
|
|
|
)
|
|
|
|
|
2019-04-12 14:19:37 +02:00
|
|
|
def pan_decrypt_event(self, event_dict, room_id=None):
|
2019-04-30 14:47:21 +02:00
|
|
|
# type: (Dict[Any, Any], Optional[str]) -> (bool)
|
2019-04-12 14:19:37 +02:00
|
|
|
event = RoomEncryptedEvent.parse_event(event_dict)
|
|
|
|
|
|
|
|
if not isinstance(event, MegolmEvent):
|
|
|
|
logger.warn("Encrypted event is not a megolm event:"
|
|
|
|
"\n{}".format(pformat(event_dict)))
|
2019-04-30 14:47:21 +02:00
|
|
|
return False
|
2019-04-12 14:19:37 +02:00
|
|
|
|
2019-04-28 21:16:14 +02:00
|
|
|
if not event.room_id:
|
|
|
|
event.room_id = room_id
|
|
|
|
|
2019-04-12 14:19:37 +02:00
|
|
|
try:
|
|
|
|
decrypted_event = self.decrypt_event(event)
|
|
|
|
logger.info("Decrypted event: {}".format(decrypted_event))
|
|
|
|
|
2019-04-30 14:47:21 +02:00
|
|
|
event_dict.update(decrypted_event.source)
|
2019-04-12 14:19:37 +02:00
|
|
|
event_dict["decrypted"] = True
|
|
|
|
event_dict["verified"] = decrypted_event.verified
|
|
|
|
|
2019-04-30 14:47:21 +02:00
|
|
|
return True
|
|
|
|
|
2019-04-12 14:19:37 +02:00
|
|
|
except EncryptionError as error:
|
|
|
|
logger.warn(error)
|
2019-04-30 14:47:21 +02:00
|
|
|
return False
|
2019-04-12 14:19:37 +02:00
|
|
|
|
|
|
|
def decrypt_messages_body(self, body):
|
|
|
|
# type: (Dict[Any, Any]) -> Dict[Any, Any]
|
|
|
|
"""Go through a messages response and decrypt megolm encrypted events.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
body (Dict[Any, Any]): The dictionary of a Sync response.
|
|
|
|
|
|
|
|
Returns the json response with decrypted events.
|
|
|
|
"""
|
|
|
|
if "chunk" not in body:
|
|
|
|
return body
|
|
|
|
|
|
|
|
logger.info("Decrypting room messages")
|
|
|
|
|
|
|
|
for event in body["chunk"]:
|
|
|
|
if "type" not in event:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if event["type"] != "m.room.encrypted":
|
|
|
|
logger.debug("Event is not encrypted: "
|
|
|
|
"\n{}".format(pformat(event)))
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.pan_decrypt_event(event)
|
|
|
|
|
|
|
|
return body
|
|
|
|
|
2019-04-03 16:48:13 +02:00
|
|
|
def decrypt_sync_body(self, body):
|
|
|
|
# type: (Dict[Any, Any]) -> Dict[Any, Any]
|
|
|
|
"""Go through a json sync response and decrypt megolm encrypted events.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
body (Dict[Any, Any]): The dictionary of a Sync response.
|
|
|
|
|
2019-04-11 14:23:48 +02:00
|
|
|
Returns the json response with decrypted events.
|
2019-04-03 16:48:13 +02:00
|
|
|
"""
|
2019-04-12 14:19:37 +02:00
|
|
|
logger.info("Decrypting sync")
|
2019-04-03 16:48:13 +02:00
|
|
|
for room_id, room_dict in body["rooms"]["join"].items():
|
2019-04-05 18:35:54 +02:00
|
|
|
try:
|
|
|
|
if not self.rooms[room_id].encrypted:
|
|
|
|
logger.info("Room {} is not encrypted skipping...".format(
|
|
|
|
self.rooms[room_id].display_name
|
|
|
|
))
|
|
|
|
continue
|
|
|
|
except KeyError:
|
|
|
|
logger.info("Unknown room {} skipping...".format(room_id))
|
2019-04-03 16:48:13 +02:00
|
|
|
continue
|
|
|
|
|
|
|
|
for event in room_dict["timeline"]["events"]:
|
2019-04-12 14:19:37 +02:00
|
|
|
self.pan_decrypt_event(event, room_id)
|
2019-04-03 16:48:13 +02:00
|
|
|
|
|
|
|
return body
|