matrix: Switch to the nio http client.

This commit is contained in:
Damir Jelić 2018-07-20 19:14:32 +02:00
parent 45be743c07
commit fc4c879e0d
6 changed files with 55 additions and 2482 deletions

46
main.py
View file

@ -32,6 +32,8 @@ from future.utils import bytes_to_native_str as n
# pylint: disable=unused-import # pylint: disable=unused-import
from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any) from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any)
from nio import TransportType
from matrix.colors import Formatted from matrix.colors import Formatted
from matrix.utf import utf8_decode from matrix.utf import utf8_decode
from matrix.http import HttpResponse from matrix.http import HttpResponse
@ -288,49 +290,31 @@ def receive_cb(server_name, file_descriptor):
server.disconnect() server.disconnect()
break break
received = len(data) # type: int server.client.receive(data)
parsed_bytes = server.http_parser.execute(data, received)
assert parsed_bytes == received response = server.client.next_response()
if server.http_parser.is_partial_body(): if response:
server.http_buffer.append(server.http_parser.recv_body()) server.handle_response(response)
if server.http_parser.is_message_complete():
status = server.http_parser.get_status_code()
headers = server.http_parser.get_headers()
body = b"".join(server.http_buffer)
message = server.receive_queue.popleft()
message.response = HttpResponse(status, headers, body)
receive_time = time.time()
server.lag = (receive_time - message.send_time) * 1000
server.lag_done = True
W.bar_item_update("lag")
message.receive_time = receive_time
prnt_debug(DebugType.MESSAGING, server,
("{prefix}Received message of type {t} and "
"status {s}").format(
prefix=W.prefix("error"),
t=message.__class__.__name__,
s=status))
# Message done, reset the parser state.
server.reset_parser()
server.handle_response(message)
break break
return W.WEECHAT_RC_OK return W.WEECHAT_RC_OK
def finalize_connection(server): def finalize_connection(server):
hook = W.hook_fd(server.socket.fileno(), 1, 0, 0, "receive_cb", server.name) hook = W.hook_fd(
server.socket.fileno(),
1,
0,
0,
"receive_cb",
server.name
)
server.fd_hook = hook server.fd_hook = hook
server.connected = True server.connected = True
server.connecting = False server.connecting = False
server.client.connect(TransportType.HTTP)
server.login() server.login()

View file

@ -1,720 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals
from builtins import str
import time
import json
from enum import Enum, unique
from functools import partial
try:
from json.decoder import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError
try:
from urllib import quote, urlencode
from urlparse import urlparse
except ImportError:
from urllib.parse import quote, urlencode, urlparse
from matrix.http import RequestType, HttpRequest
import matrix.events as MatrixEvents
MATRIX_API_PATH = "/_matrix/client/r0" # type: str
class MatrixClient:
def __init__(
self,
host, # type: str
access_token="", # type: str
user_agent="" # type: str
):
# type: (...) -> None
self.host = host
self.user_agent = user_agent
self.access_token = access_token
self.txn_id = 0 # type: int
def _get_txn_id(self):
txn_id = self.txn_id
self.txn_id = self.txn_id + 1
return txn_id
def login(self, user, password, device_name="", device_id=""):
# type (str, str, str) -> HttpRequest
path = ("{api}/login").format(api=MATRIX_API_PATH)
post_data = {
"type": "m.login.password",
"user": user,
"password": password
}
if device_id:
post_data["device_id"] = device_id
if device_name:
post_data["initial_device_display_name"] = device_name
return HttpRequest(RequestType.POST, self.host, path, post_data)
def sync(self, next_batch="", sync_filter=None):
# type: (str, Dict[Any, Any]) -> HttpRequest
assert self.access_token
query_parameters = {"access_token": self.access_token}
if sync_filter:
query_parameters["filter"] = json.dumps(
sync_filter, separators=(",", ":"))
if next_batch:
query_parameters["since"] = next_batch
path = ("{api}/sync?{query_params}").format(
api=MATRIX_API_PATH, query_params=urlencode(query_parameters))
return HttpRequest(RequestType.GET, self.host, path)
def room_encrypted_message(self, room_id, content):
# type: (str, Dict[Any, Any]) -> HttpRequest
query_parameters = {"access_token": self.access_token}
path = ("{api}/rooms/{room}/send/m.room.encrypted/"
"{tx_id}?{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
def room_send_message(self,
room_id,
message_type,
content,
formatted_content=None):
# type: (str, str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
body = {"msgtype": message_type, "body": content}
if formatted_content:
body["format"] = "org.matrix.custom.html"
body["formatted_body"] = formatted_content
path = ("{api}/rooms/{room}/send/m.room.message/"
"{tx_id}?{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, body)
def room_topic(self, room_id, topic):
# type: (str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
content = {"topic": topic}
path = ("{api}/rooms/{room}/state/m.room.topic?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
def room_redact(self, room_id, event_id, reason=None):
# type: (str, str, str) -> HttpRequest
query_parameters = {"access_token": self.access_token}
content = {}
if reason:
content["reason"] = reason
path = ("{api}/rooms/{room}/redact/{event_id}/{tx_id}?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
event_id=quote(event_id),
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
def room_get_messages(self,
room_id,
start_token,
end_token="",
limit=10,
direction='b'):
query_parameters = {
"access_token": self.access_token,
"from": start_token,
"dir": direction,
"limit": str(limit)
}
if end_token:
query_parameters["to"] = end_token
path = ("{api}/rooms/{room}/messages?{query_parameters}").format(
api=MATRIX_API_PATH,
room=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.GET, self.host, path)
def room_join(self, room_id):
query_parameters = {"access_token": self.access_token}
path = ("{api}/join/{room_id}?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path)
def room_leave(self, room_id):
query_parameters = {"access_token": self.access_token}
path = ("{api}/rooms/{room_id}/leave?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path)
def room_invite(self, room_id, user_id):
query_parameters = {"access_token": self.access_token}
content = {"user_id": user_id}
path = ("{api}/rooms/{room_id}/invite?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.POST, self.host, path, content)
def room_kick(self, room_id, user_id, reason=None):
query_parameters = {"access_token": self.access_token}
content = {"user_id": user_id}
if reason:
content["reason"] = reason
path = ("{api}/rooms/{room_id}/kick?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
room_id=quote(room_id),
query_parameters=urlencode(query_parameters))
h = HttpRequest(RequestType.POST, self.host, path, content)
return h
def keys_upload(self, user_id, device_id, olm, keys=None,
one_time_keys=None):
query_parameters = {"access_token": self.access_token}
path = ("{api}/keys/upload?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
query_parameters=urlencode(query_parameters))
content = {}
if keys:
device_keys = {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": device_id,
"user_id": user_id,
"keys": {
"curve25519:" + device_id: keys["curve25519"],
"ed25519:" + device_id: keys["ed25519"]
}
}
signature = olm.sign_json(device_keys)
device_keys["signatures"] = {
user_id: {
"ed25519:" + device_id: signature
}
}
content["device_keys"] = device_keys
if one_time_keys:
one_time_key_dict = {}
for key_id, key in one_time_keys.items():
key_dict = {"key": key}
signature = olm.sign_json(key_dict)
one_time_key_dict["signed_curve25519:" + key_id] = {
"key": key_dict.pop("key"),
"signatures": {
user_id: {
"ed25519:" + device_id: signature
}
}
}
content["one_time_keys"] = one_time_key_dict
return HttpRequest(RequestType.POST, self.host, path, content)
def keys_query(self, users):
query_parameters = {"access_token": self.access_token}
path = ("{api}/keys/query?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
query_parameters=urlencode(query_parameters))
content = {
"device_keys": {user: {} for user in users}
}
return HttpRequest(RequestType.POST, self.host, path, content)
def keys_claim(self, key_dict):
query_parameters = {"access_token": self.access_token}
path = ("{api}/keys/claim?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
query_parameters=urlencode(query_parameters))
content = {
"one_time_keys": key_dict,
"timeout": 10000
}
return HttpRequest(RequestType.POST, self.host, path, content)
def to_device(self, event_type, content):
query_parameters = {"access_token": self.access_token}
path = ("{api}/sendToDevice/{event_type}/{tx_id}?"
"{query_parameters}").format(
api=MATRIX_API_PATH,
event_type=event_type,
tx_id=quote(str(self._get_txn_id())),
query_parameters=urlencode(query_parameters))
return HttpRequest(RequestType.PUT, self.host, path, content)
@staticmethod
def mxc_to_http(mxc):
# type: (str) -> str
url = urlparse(mxc)
if url.scheme != "mxc":
return None
if not url.netloc or not url.path:
return None
return "https://{}/_matrix/media/r0/download/{}{}".format(
url.netloc,
url.netloc,
url.path
)
class MatrixMessage():
def __init__(
self,
request_func, # type: Callable[[...], HttpRequest]
func_args,
):
# type: (...) -> None
# yapf: disable
self.request = None # type: HttpRequest
self.response = None # type: HttpResponse
self.decoded_response = None # type: Dict[Any, Any]
self.creation_time = time.time() # type: float
self.send_time = None # type: float
self.receive_time = None # type: float
self.event = None
self.request = request_func(**func_args)
# yapf: enable
def decode_body(self, server):
try:
self.decoded_response = json.loads(
self.response.body, encoding='utf-8')
return (True, None)
except Exception as error:
return (False, error)
def _decode(self, server, object_hook):
try:
parsed_dict = json.loads(
self.response.body,
encoding='utf-8',
)
self.event = object_hook(parsed_dict)
return (True, None)
except JSONDecodeError as error:
return (False, error)
class MatrixLoginMessage(MatrixMessage):
def __init__(self, client, user, password, device_name, device_id=None):
data = {"user": user, "password": password, "device_name": device_name}
if device_id:
data["device_id"] = device_id
MatrixMessage.__init__(self, client.login, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixLoginEvent.from_dict, server)
return self._decode(server, object_hook)
class MatrixSyncMessage(MatrixMessage):
def __init__(self, client, next_batch=None, limit=None):
data = {}
if next_batch:
data["next_batch"] = next_batch
if limit:
data["sync_filter"] = {"room": {"timeline": {"limit": limit}}}
MatrixMessage.__init__(self, client.sync, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixSyncEvent.from_dict, server)
return self._decode(server, object_hook)
class MatrixSendMessage(MatrixMessage):
def __init__(self,
client,
room_id,
formatted_message,
message_type="m.text"):
self.room_id = room_id
self.formatted_message = formatted_message
data = {
"room_id": self.room_id,
"message_type": message_type,
"content": self.formatted_message.to_plain()
}
if self.formatted_message.is_formatted():
data["formatted_content"] = self.formatted_message.to_html()
MatrixMessage.__init__(self, client.room_send_message, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixSendEvent.from_dict,
server,
self.room_id,
self.formatted_message,
)
return self._decode(server, object_hook)
class MatrixEmoteMessage(MatrixSendMessage):
def __init__(self, client, room_id, formatted_message):
MatrixSendMessage.__init__(self, client, room_id, formatted_message,
"m.emote")
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixEmoteEvent.from_dict,
server,
self.room_id,
self.formatted_message,
)
return self._decode(server, object_hook)
class MatrixTopicMessage(MatrixMessage):
def __init__(self, client, room_id, topic):
self.room_id = room_id
self.topic = topic
data = {"room_id": self.room_id, "topic": self.topic}
MatrixMessage.__init__(self, client.room_topic, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixTopicEvent.from_dict,
server,
self.room_id,
self.topic,
)
return self._decode(server, object_hook)
class MatrixRedactMessage(MatrixMessage):
def __init__(self, client, room_id, event_id, reason=None):
self.room_id = room_id
self.event_id = event_id
self.reason = reason
data = {"room_id": self.room_id, "event_id": self.event_id}
if reason:
data["reason"] = reason
MatrixMessage.__init__(self, client.room_redact, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixRedactEvent.from_dict,
server,
self.room_id,
self.reason,
)
return self._decode(server, object_hook)
class MatrixBacklogMessage(MatrixMessage):
def __init__(self, client, room_id, token, limit):
self.room_id = room_id
data = {
"room_id": self.room_id,
"start_token": token,
"direction": "b",
"limit": limit
}
MatrixMessage.__init__(self, client.room_get_messages, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixBacklogEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixJoinMessage(MatrixMessage):
def __init__(self, client, room_id):
self.room_id = room_id
data = {"room_id": self.room_id}
MatrixMessage.__init__(self, client.room_join, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixJoinEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixPartMessage(MatrixMessage):
def __init__(self, client, room_id):
self.room_id = room_id
data = {"room_id": self.room_id}
MatrixMessage.__init__(self, client.room_leave, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixPartEvent.from_dict, server,
self.room_id)
return self._decode(server, object_hook)
class MatrixInviteMessage(MatrixMessage):
def __init__(self, client, room_id, user_id):
self.room_id = room_id
self.user_id = user_id
data = {"room_id": self.room_id, "user_id": self.user_id}
MatrixMessage.__init__(self, client.room_invite, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixInviteEvent.from_dict, server,
self.room_id, self.user_id)
return self._decode(server, object_hook)
class MatrixKickMessage(MatrixMessage):
def __init__(self, client, room_id, user_id, reason=None):
self.room_id = room_id
self.user_id = user_id
self.reason = reason
data = {"room_id": self.room_id,
"user_id": self.user_id,
"reason": reason}
MatrixMessage.__init__(self, client.room_kick, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixKickEvent.from_dict,
server,
self.room_id,
self.user_id,
self.reason)
return self._decode(server, object_hook)
class MatrixKeyUploadMessage(MatrixMessage):
def __init__(self, client, user_id, device_id, olm, keys=None,
one_time_keys=None):
data = {
"device_id": device_id,
"user_id": user_id,
"olm": olm,
"keys": keys,
"one_time_keys": one_time_keys
}
self.device_keys = True if keys else False
MatrixMessage.__init__(self, client.keys_upload, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixKeyUploadEvent.from_dict,
server, self.device_keys)
return self._decode(server, object_hook)
class MatrixKeyQueryMessage(MatrixMessage):
def __init__(self, client, users):
data = {
"users": users,
}
MatrixMessage.__init__(self, client.keys_query, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixKeyQueryEvent.from_dict,
server)
return self._decode(server, object_hook)
class MatrixKeyClaimMessage(MatrixMessage):
def __init__(self, client, room_id, key_dict):
self.room_id = room_id
data = {
"key_dict": key_dict,
}
MatrixMessage.__init__(self, client.keys_claim, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixKeyClaimEvent.from_dict,
server, self.room_id)
return self._decode(server, object_hook)
class MatrixToDeviceMessage(MatrixMessage):
def __init__(self, client, to_device_dict):
data = {
"content": to_device_dict,
"event_type": "m.room.encrypted"
}
MatrixMessage.__init__(self, client.to_device, data)
def decode_body(self, server):
object_hook = partial(MatrixEvents.MatrixToDeviceEvent.from_dict,
server)
return self._decode(server, object_hook)
class MatrixEncryptedMessage(MatrixMessage):
def __init__(self,
client,
room_id,
formatted_message,
content):
self.room_id = room_id
self.formatted_message = formatted_message
data = {
"room_id": self.room_id,
"content": content
}
MatrixMessage.__init__(self, client.room_encrypted_message, data)
def decode_body(self, server):
object_hook = partial(
MatrixEvents.MatrixSendEvent.from_dict,
server,
self.room_id,
self.formatted_message,
)
return self._decode(server, object_hook)

View file

@ -1,913 +0,0 @@
# -*- coding: utf-8 -*-
# Weechat Matrix Protocol Script
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals
import os
import json
import sqlite3
import pprint
import argparse
# pylint: disable=redefined-builtin
from builtins import str, bytes
from collections import defaultdict
from itertools import chain
from functools import wraps
from future.moves.itertools import zip_longest
try:
FileNotFoundError
except NameError:
FileNotFoundError = IOError
import matrix.globals
try:
from olm import (
Account,
OlmAccountError,
Session,
InboundSession,
OutboundSession,
OlmSessionError,
OlmPreKeyMessage,
InboundGroupSession,
OutboundGroupSession,
OlmGroupSessionError
)
except ImportError:
matrix.globals.ENCRYPTION = False
from matrix.globals import W, SERVERS
from matrix.utils import sanitize_id
from matrix.utf import utf8_decode
class ParseError(Exception):
pass
class OlmTrustError(Exception):
pass
class WeechatArgParse(argparse.ArgumentParser):
def print_usage(self, file):
pass
def error(self, message):
m = ("{prefix}Error: {message} for command {command} "
"(see /help {command})").format(prefix=W.prefix("error"),
message=message,
command=self.prog)
W.prnt("", m)
raise ParseError
def own_buffer_or_error(f):
@wraps(f)
def wrapper(data, buffer, *args, **kwargs):
for server in SERVERS.values():
if buffer in server.buffers.values():
return f(server.name, buffer, *args, **kwargs)
elif buffer == server.server_buffer:
return f(server.name, buffer, *args, **kwargs)
W.prnt("", "{prefix}matrix: command \"olm\" must be executed on a "
"matrix buffer (server or channel)".format(
prefix=W.prefix("error")))
return W.WEECHAT_RC_OK
return wrapper
def encrypt_enabled(f):
@wraps(f)
def wrapper(*args, **kwds):
if matrix.globals.ENCRYPTION:
return f(*args, **kwds)
return None
return wrapper
@encrypt_enabled
def matrix_hook_olm_command():
W.hook_command(
# Command name and short description
"olm",
"Matrix olm encryption command",
# Synopsis
("info all|blacklisted|private|unverified|verified <filter>||"
"blacklist <user-id> <device-id> ||"
"unverify <user-id> <device-id> ||"
"verify <user-id> <device-id>"),
# Description
(" info: show info about known devices and their keys\n"
"blacklist: blacklist a device\n"
" unverify: unverify a device\n"
" verify: verify a device\n\n"
"Examples:\n"),
# Completions
('info all|blacklisted|private|unverified|verified ||'
'blacklist %(device_ids) ||'
'unverify %(user_ids) %(device_ids) ||'
'verify %(olm_user_ids) %(olm_devices)'),
# Function name
'matrix_olm_command_cb',
'')
def olm_cmd_parse_args(args):
parser = WeechatArgParse(prog="olm")
subparsers = parser.add_subparsers(dest="subcommand")
info_parser = subparsers.add_parser("info")
info_parser.add_argument(
"category", nargs="?", default="private",
choices=[
"all",
"blacklisted",
"private",
"unverified",
"verified"
])
info_parser.add_argument("filter", nargs="?")
verify_parser = subparsers.add_parser("verify")
verify_parser.add_argument("user_filter")
verify_parser.add_argument("device_filter", nargs="?")
try:
parsed_args = parser.parse_args(args.split())
return parsed_args
except ParseError:
return None
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
def partition_key(key):
groups = grouper(key, 4, " ")
return ' '.join(''.join(g) for g in groups)
def olm_info_command(server, args):
olm = server.olm
if args.category == "private":
device_msg = (" - Device ID: {}\n".format(server.device_id)
if server.device_id else "")
id_key = partition_key(olm.account.identity_keys["curve25519"])
fp_key = partition_key(olm.account.identity_keys["ed25519"])
message = ("{prefix}matrix: Identity keys:\n"
" - User: {user}\n"
"{device_msg}"
" - Identity key: {id_key}\n"
" - Fingerprint key: {fp_key}\n").format(
prefix=W.prefix("network"),
user=server.user,
device_msg=device_msg,
id_key=id_key,
fp_key=fp_key)
W.prnt(server.server_buffer, message)
elif args.category == "all":
for user, keys in olm.device_keys.items():
message = ("{prefix}matrix: Identity keys:\n"
" - User: {user}\n").format(
prefix=W.prefix("network"),
user=user)
W.prnt(server.server_buffer, message)
for key in keys:
id_key = partition_key(key.keys["curve25519"])
fp_key = partition_key(key.keys["ed25519"])
device_msg = (" - Device ID: {}\n".format(
key.device_id) if key.device_id else "")
message = ("{device_msg}"
" - Identity key: {id_key}\n"
" - Fingerprint key: {fp_key}\n\n").format(
device_msg=device_msg,
id_key=id_key,
fp_key=fp_key)
W.prnt(server.server_buffer, message)
def olm_verify_command(server, args):
olm = server.olm
devices = olm.device_keys
filtered_devices = []
if args.user_filter == "*":
filtered_devices = devices.values()
else:
device_keys = filter(lambda x: args.user_filter in x, devices)
filtered_devices = [devices[x] for x in device_keys]
filtered_devices = chain.from_iterable(filtered_devices)
if args.device_filter and args.device_filter != "*":
filtered_devices = filter(
lambda x: args.device_filter in x.device_id,
filtered_devices
)
key_list = []
for device in list(filtered_devices):
if olm.verify_device(device):
key_list.append(str(device))
if not key_list:
message = ("{prefix}matrix: No matching unverified devices "
"found.").format(prefix=W.prefix("error"))
W.prnt(server.server_buffer, message)
return
key_str = "\n - ".join(key_list)
message = ("{prefix}matrix: Verified keys:\n"
" - {key_str}").format(prefix=W.prefix("join"),
key_str=key_str)
W.prnt(server.server_buffer, message)
@own_buffer_or_error
@utf8_decode
def matrix_olm_command_cb(server_name, buffer, args):
server = SERVERS[server_name]
parsed_args = olm_cmd_parse_args(args)
if not parsed_args:
return W.WEECHAT_RC_OK
if not parsed_args.subcommand or parsed_args.subcommand == "info":
olm_info_command(server, parsed_args)
elif parsed_args.subcommand == "verify":
olm_verify_command(server, parsed_args)
else:
message = ("{prefix}matrix: Command not implemented.".format(
prefix=W.prefix("error")))
W.prnt(server.server_buffer, message)
return W.WEECHAT_RC_OK
class EncryptionError(Exception):
pass
class DeviceStore(object):
def __init__(self, filename):
# type: (str) -> None
self._entries = []
self._filename = filename
self._load(filename)
def _load(self, filename):
# type: (str) -> None
try:
with open(filename, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
entry = StoreEntry.from_line(line)
if not entry:
continue
self._entries.append(entry)
except FileNotFoundError:
pass
def _save_store(f):
@wraps(f)
def decorated(*args, **kwargs):
self = args[0]
ret = f(*args, **kwargs)
self._save()
return ret
return decorated
def _save(self):
# type: (str) -> None
with open(self._filename, "w") as f:
for entry in self._entries:
line = entry.to_line()
f.write(line)
@_save_store
def add(self, device):
# type: (OlmDeviceKey) -> None
new_entries = StoreEntry.from_olmdevice(device)
self._entries += new_entries
# Remove duplicate entries
self._entries = list(set(self._entries))
self._save()
@_save_store
def remove(self, device):
# type: (OlmDeviceKey) -> int
removed = 0
entries = StoreEntry.from_olmdevice(device)
for entry in entries:
if entry in self._entries:
self._entries.remove(entry)
removed += 1
self._save()
return removed
def check(self, device):
# type: (OlmDeviceKey) -> bool
entries = StoreEntry.from_olmdevice(device)
result = map(lambda entry: entry in self._entries, entries)
if False in result:
return False
return True
class StoreEntry(object):
def __init__(self, user_id, device_id, key_type, key):
# type: (str, str, str, str) -> None
self.user_id = user_id
self.device_id = device_id
self.key_type = key_type
self.key = key
@classmethod
def from_line(cls, line):
# type: (str) -> StoreEntry
fields = line.split(' ')
if len(fields) < 4:
return None
user_id, device_id, key_type, key = fields[:4]
if key_type == "matrix-ed25519":
return cls(user_id, device_id, "ed25519", key)
else:
return None
@classmethod
def from_olmdevice(cls, device_key):
# type: (OlmDeviceKey) -> [StoreEntry]
entries = []
user_id = device_key.user_id
device_id = device_key.device_id
for key_type, key in device_key.keys.items():
if key_type == "ed25519":
entries.append(cls(user_id, device_id, "ed25519", key))
return entries
def to_line(self):
# type: () -> str
key_type = "matrix-{}".format(self.key_type)
line = "{} {} {} {}\n".format(
self.user_id,
self.device_id,
key_type,
self.key
)
return line
def __hash__(self):
# type: () -> int
return hash(str(self))
def __str__(self):
# type: () -> str
key_type = "matrix-{}".format(self.key_type)
line = "{} {} {} {}".format(
self.user_id,
self.device_id,
key_type,
self.key
)
return line
def __eq__(self, value):
# type: (StoreEntry) -> bool
if (self.user_id == value.user_id
and self.device_id == value.device_id
and self.key_type == value.key_type and self.key == value.key):
return True
return False
class OlmDeviceKey():
def __init__(self, user_id, device_id, key_dict):
# type: (str, str, Dict[str, str])
self.user_id = user_id
self.device_id = device_id
self.keys = key_dict
def __str__(self):
# type: () -> str
return "{} {} {}".format(
self.user_id, self.device_id, self.keys["ed25519"])
def __repr__(self):
# type: () -> str
return str(self)
class OneTimeKey():
def __init__(self, user_id, device_id, key):
# type: (str, str, str) -> None
self.user_id = user_id
self.device_id = device_id
self.key = key
class Olm():
@encrypt_enabled
def __init__(
self,
user,
device_id,
session_path,
database=None,
account=None,
sessions=None,
inbound_group_sessions=None
):
# type: (str, str, str, Account, Dict[str, List[Session]) -> None
self.user = user
self.device_id = device_id
self.session_path = session_path
self.database = database
self.device_keys = {}
self.shared_sessions = []
if not database:
db_file = "{}_{}.db".format(user, device_id)
db_path = os.path.join(session_path, db_file)
self.database = sqlite3.connect(db_path)
Olm._check_db_tables(self.database)
if account:
self.account = account
else:
self.account = Account()
self._insert_acc_to_db()
if not sessions:
sessions = defaultdict(lambda: defaultdict(list))
if not inbound_group_sessions:
inbound_group_sessions = defaultdict(dict)
self.sessions = sessions
self.inbound_group_sessions = inbound_group_sessions
self.outbound_group_sessions = {}
trust_file_path = "{}_{}.trusted_devices".format(user, device_id)
self.trust_db = DeviceStore(os.path.join(session_path, trust_file_path))
def _create_session(self, sender, sender_key, message):
W.prnt("", "matrix: Creating session for {}".format(sender))
session = InboundSession(self.account, message, sender_key)
W.prnt("", "matrix: Created session for {}".format(sender))
self.account.remove_one_time_keys(session)
self._update_acc_in_db()
return session
def verify_device(self, device):
if self.trust_db.check(device):
return False
self.trust_db.add(device)
return True
def unverify_device(self, device):
self.trust_db.remove(device)
def create_session(self, user_id, device_id, one_time_key):
W.prnt("", "matrix: Creating session for {}".format(user_id))
id_key = None
for user, keys in self.device_keys.items():
if user != user_id:
continue
for key in keys:
if key.device_id == device_id:
id_key = key.keys["curve25519"]
break
if not id_key:
W.prnt("", "ERRR not found ID key")
W.prnt("", "Found id key {}".format(id_key))
session = OutboundSession(self.account, id_key, one_time_key)
self._update_acc_in_db()
self.sessions[user_id][device_id].append(session)
self._store_session(user_id, device_id, session)
W.prnt("", "matrix: Created session for {}".format(user_id))
def create_group_session(self, room_id, session_id, session_key):
W.prnt("", "matrix: Creating group session for {}".format(room_id))
session = InboundGroupSession(session_key)
self.inbound_group_sessions[room_id][session_id] = session
self._store_inbound_group_session(room_id, session)
def create_outbound_group_session(self, room_id):
session = OutboundGroupSession()
self.outbound_group_sessions[room_id] = session
self.create_group_session(room_id, session.id, session.session_key)
@encrypt_enabled
def get_missing_sessions(self, users):
# type: (List[str]) -> Dict[str, Dict[str, str]]
missing = {}
for user in users:
devices = []
for key in self.device_keys[user]:
# we don't need a session for our own device, skip it
if key.device_id == self.device_id:
continue
if not self.sessions[user][key.device_id]:
W.prnt("", "Missing session for device {}".format(key.device_id))
devices.append(key.device_id)
if devices:
missing[user] = {device: "signed_curve25519" for
device in devices}
return missing
@encrypt_enabled
def decrypt(self, sender, sender_key, message):
plaintext = None
for device_id, session_list in self.sessions[sender].items():
for session in session_list:
W.prnt("", "Trying session for device {}".format(device_id))
try:
if isinstance(message, OlmPreKeyMessage):
if not session.matches(message):
continue
W.prnt("", "Decrypting using existing session")
plaintext = session.decrypt(message)
parsed_plaintext = json.loads(plaintext, encoding='utf-8')
W.prnt("", "Decrypted using existing session")
return parsed_plaintext
except OlmSessionError:
pass
try:
session = self._create_session(sender, sender_key, message)
except OlmSessionError:
return None
try:
plaintext = session.decrypt(message)
parsed_plaintext = json.loads(plaintext, encoding='utf-8')
device_id = sanitize_id(parsed_plaintext["sender_device"])
self.sessions[sender][device_id].append(session)
self._store_session(sender, device_id, session)
return parsed_plaintext
except OlmSessionError:
return None
def group_encrypt(self, room_id, plaintext_dict, own_id, users):
# type: (str, Dict[str, str]) -> Dict[str, str], Optional[Dict[Any, Any]]
plaintext_dict["room_id"] = room_id
to_device_dict = None
if room_id not in self.outbound_group_sessions:
self.create_outbound_group_session(room_id)
if self.outbound_group_sessions[room_id].id not in self.shared_sessions:
to_device_dict = self.share_group_session(room_id, own_id, users)
self.shared_sessions.append(
self.outbound_group_sessions[room_id].id
)
session = self.outbound_group_sessions[room_id]
ciphertext = session.encrypt(Olm._to_json(plaintext_dict))
payload_dict = {
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": self.account.identity_keys["curve25519"],
"ciphertext": ciphertext,
"session_id": session.id,
"device_id": self.device_id
}
return payload_dict, to_device_dict
@encrypt_enabled
def group_decrypt(self, room_id, session_id, ciphertext):
if session_id not in self.inbound_group_sessions[room_id]:
return None, None
session = self.inbound_group_sessions[room_id][session_id]
try:
return session.decrypt(ciphertext)
except OlmGroupSessionError:
pass
return None, None
def share_group_session(self, room_id, own_id, users):
group_session = self.outbound_group_sessions[room_id]
key_content = {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": room_id,
"session_id": group_session.id,
"session_key": group_session.session_key,
"chain_index": group_session.message_index
}
payload_dict = {
"type": "m.room_key",
"content": key_content,
# TODO we don't have the user_id in the Olm class
"sender": own_id,
"sender_device": self.device_id,
"keys": {
"ed25519": self.account.identity_keys["ed25519"]
}
}
to_device_dict = {
"messages": {}
}
for user in users:
if user not in self.device_keys:
continue
for key in self.device_keys[user]:
if key.device_id == self.device_id:
continue
if not self.sessions[user][key.device_id]:
continue
if not self.trust_db.check(key):
raise OlmTrustError
device_payload_dict = payload_dict.copy()
# TODO sort the sessions
session = self.sessions[user][key.device_id][0]
device_payload_dict["recipient"] = user
device_payload_dict["recipient_keys"] = {
"ed25519": key.keys["ed25519"]
}
olm_message = session.encrypt(
Olm._to_json(device_payload_dict)
)
olm_dict = {
"algorithm": "m.olm.v1.curve25519-aes-sha2",
"sender_key": self.account.identity_keys["curve25519"],
"ciphertext": {
key.keys["curve25519"]: {
"type": (0 if isinstance(
olm_message,
OlmPreKeyMessage
) else 1),
"body": olm_message.ciphertext
}
}
}
if user not in to_device_dict["messages"]:
to_device_dict["messages"][user] = {}
to_device_dict["messages"][user][key.device_id] = olm_dict
# W.prnt("", pprint.pformat(to_device_dict))
return to_device_dict
@classmethod
@encrypt_enabled
def from_session_dir(cls, user, device_id, session_path):
# type: (Server) -> Olm
db_file = "{}_{}.db".format(user, device_id)
db_path = os.path.join(session_path, db_file)
database = sqlite3.connect(db_path)
Olm._check_db_tables(database)
cursor = database.cursor()
cursor.execute("select pickle from olmaccount where user = ?", (user,))
row = cursor.fetchone()
account_pickle = row[0]
cursor.execute("select user, device_id, pickle from olmsessions")
db_sessions = cursor.fetchall()
cursor.execute("select room_id, pickle from inbound_group_sessions")
db_inbound_group_sessions = cursor.fetchall()
cursor.close()
sessions = defaultdict(lambda: defaultdict(list))
inbound_group_sessions = defaultdict(dict)
try:
try:
account_pickle = bytes(account_pickle, "utf-8")
except TypeError:
pass
account = Account.from_pickle(account_pickle)
for db_session in db_sessions:
session_pickle = db_session[2]
try:
session_pickle = bytes(session_pickle, "utf-8")
except TypeError:
pass
sessions[db_session[0]][db_session[1]].append(
Session.from_pickle(session_pickle))
for db_session in db_inbound_group_sessions:
session_pickle = db_session[1]
try:
session_pickle = bytes(session_pickle, "utf-8")
except TypeError:
pass
session = InboundGroupSession.from_pickle(session_pickle)
inbound_group_sessions[db_session[0]][session.id] = session
return cls(user, device_id, session_path, database, account,
sessions, inbound_group_sessions)
except (OlmAccountError, OlmSessionError) as error:
raise EncryptionError(error)
def _update_acc_in_db(self):
cursor = self.database.cursor()
cursor.execute("update olmaccount set pickle=? where user = ?",
(self.account.pickle(), self.user))
self.database.commit()
cursor.close()
def _update_sessions_in_db(self):
cursor = self.database.cursor()
for user, session_dict in self.sessions.items():
for device_id, session_list in session_dict.items():
for session in session_list:
cursor.execute("""update olmsessions set pickle=?
where user = ? and session_id = ? and
device_id = ?""",
(session.pickle(), user, session.id,
device_id))
self.database.commit()
cursor.close()
def _update_inbound_group_sessions(self):
cursor = self.database.cursor()
for room_id, session_dict in self.inbound_group_sessions.items():
for session in session_dict.values():
cursor.execute("""update inbound_group_sessions set pickle=?
where room_id = ? and session_id = ?""",
(session.pickle(), room_id, session.id))
self.database.commit()
cursor.close()
def _store_session(self, user, device_id, session):
cursor = self.database.cursor()
cursor.execute("insert into olmsessions values(?,?,?,?)",
(user, device_id, session.id, session.pickle()))
self.database.commit()
cursor.close()
def _store_inbound_group_session(self, room_id, session):
cursor = self.database.cursor()
cursor.execute("insert into inbound_group_sessions values(?,?,?)",
(room_id, session.id, session.pickle()))
self.database.commit()
cursor.close()
def _insert_acc_to_db(self):
cursor = self.database.cursor()
cursor.execute("insert into olmaccount values (?,?)",
(self.user, self.account.pickle()))
self.database.commit()
cursor.close()
@staticmethod
def _check_db_tables(database):
cursor = database.cursor()
cursor.execute("""select name from sqlite_master where type='table'
and name='olmaccount'""")
if not cursor.fetchone():
cursor.execute("create table olmaccount (user text, pickle text)")
database.commit()
cursor.execute("""select name from sqlite_master where type='table'
and name='olmsessions'""")
if not cursor.fetchone():
cursor.execute("""create table olmsessions (user text,
device_id text, session_id text, pickle text)""")
database.commit()
cursor.execute("""select name from sqlite_master where type='table'
and name='inbound_group_sessions'""")
if not cursor.fetchone():
cursor.execute("""create table inbound_group_sessions
(room_id text, session_id text, pickle text)""")
database.commit()
cursor.close()
@encrypt_enabled
def to_session_dir(self):
# type: (Server) -> None
try:
self._update_acc_in_db()
self._update_sessions_in_db()
except OlmAccountError as error:
raise EncryptionError(error)
def sign_json(self, json_dict):
signature = self.account.sign(json.dumps(
json_dict,
ensure_ascii=False,
separators=(',', ':'),
sort_keys=True,
))
return signature
@staticmethod
def _to_json(json_dict):
# type: (Dict[Any, Any]) -> str
return json.dumps(
json_dict,
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True
)
@encrypt_enabled
def mark_keys_as_published(self):
self.account.mark_keys_as_published()

View file

@ -1,386 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals
from builtins import str
import json
import pprint
from collections import deque, defaultdict
from functools import partial
from operator import itemgetter
from matrix.globals import W
from matrix.utils import (tags_for_message, sanitize_id, sanitize_token,
sanitize_text, tags_from_line_data)
from matrix.encryption import OlmDeviceKey, OneTimeKey
from .buffer import RoomUser, OwnMessage, OwnAction
try:
from olm.session import OlmMessage, OlmPreKeyMessage
except ImportError:
pass
class MatrixEvent():
def __init__(self, server):
self.server = server
def execute(self):
pass
class MatrixErrorEvent(MatrixEvent):
def __init__(self, server, error_message, fatal=False):
self.error_message = error_message
self.fatal = fatal
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, error_prefix, fatal, parsed_dict):
try:
message = "{prefix}: {error}".format(
prefix=error_prefix, error=sanitize_text(parsed_dict["error"]))
return cls(server, message, fatal=fatal)
except KeyError:
return cls(
server, ("{prefix}: Invalid JSON response "
"from server.").format(prefix=error_prefix),
fatal=fatal)
class MatrixKeyUploadEvent(MatrixEvent):
def __init__(self, server, device_keys):
self.device_keys = device_keys
MatrixEvent.__init__(self, server)
def execute(self):
self.server.olm.mark_keys_as_published()
self.server.store_olm()
if not self.device_keys:
return
message = "{prefix}matrix: Uploaded Olm device keys.".format(
prefix=W.prefix("network"))
W.prnt(self.server.server_buffer, message)
@classmethod
def from_dict(cls, server, device_keys, parsed_dict):
try:
return cls(server, device_keys)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error uploading device"
"keys", False, parsed_dict)
class MatrixSendEvent(MatrixEvent):
def __init__(self, server, room_id, message):
self.room_id = room_id
self.message = message
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, message, parsed_dict):
try:
event_id = sanitize_id(parsed_dict["event_id"])
sender = server.user_id
age = 0
formatted_message = message
message = OwnMessage(sender, age, event_id, formatted_message)
return cls(server, room_id, message)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error sending message",
False, parsed_dict)
class MatrixEmoteEvent(MatrixSendEvent):
@classmethod
def from_dict(cls, server, room_id, message, parsed_dict):
try:
event_id = sanitize_id(parsed_dict["event_id"])
sender = server.user_id
age = 0
formatted_message = message
message = OwnAction(sender, age, event_id, formatted_message)
return cls(server, room_id, message)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error sending message",
False, parsed_dict)
class MatrixTopicEvent(MatrixEvent):
def __init__(self, server, room_id, event_id, topic):
self.room_id = room_id
self.topic = topic
self.event_id = event_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, topic, parsed_dict):
try:
return cls(server, room_id, sanitize_id(parsed_dict["event_id"]),
topic)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error setting topic",
False, parsed_dict)
class MatrixRedactEvent(MatrixEvent):
def __init__(self, server, room_id, event_id, reason):
self.room_id = room_id
self.topic = reason
self.event_id = event_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, reason, parsed_dict):
try:
return cls(server, room_id, sanitize_id(parsed_dict["event_id"]),
reason)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error redacting message",
False, parsed_dict)
class MatrixJoinEvent(MatrixEvent):
def __init__(self, server, room, room_id):
self.room = room
self.room_id = room_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
return cls(
server,
room_id,
sanitize_id(parsed_dict["room_id"]),
)
except (KeyError, TypeError, ValueError):
return MatrixErrorEvent.from_dict(server, "Error joining room",
False, parsed_dict)
class MatrixPartEvent(MatrixEvent):
def __init__(self, server, room_id):
self.room_id = room_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error leaving room",
False, parsed_dict)
class MatrixInviteEvent(MatrixEvent):
def __init__(self, server, room_id, user_id):
self.room_id = room_id
self.user_id = user_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, user_id, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id, user_id)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error inviting user",
False, parsed_dict)
class MatrixKickEvent(MatrixEvent):
def __init__(self, server, room_id, user_id, reason):
self.room_id = room_id
self.user_id = user_id
self.reason = reason
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, user_id, reason, parsed_dict):
try:
if parsed_dict == {}:
return cls(server, room_id, user_id, reason)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, "Error kicking user",
False, parsed_dict)
class MatrixKeyQueryEvent(MatrixEvent):
def __init__(self, server, keys):
self.keys = keys
MatrixEvent.__init__(self, server)
@staticmethod
def _get_keys(key_dict):
keys = {}
for key_type, key in key_dict.items():
key_type, _ = key_type.split(":")
keys[key_type] = key
return keys
@classmethod
def from_dict(cls, server, parsed_dict):
keys = defaultdict(list)
try:
for user_id, device_dict in parsed_dict["device_keys"].items():
for device_id, key_dict in device_dict.items():
device_keys = MatrixKeyQueryEvent._get_keys(
key_dict.pop("keys"))
keys[user_id].append(OlmDeviceKey(user_id, device_id,
device_keys))
return cls(server, keys)
except KeyError:
# TODO error message
return MatrixErrorEvent.from_dict(server, "Error kicking user",
False, parsed_dict)
def execute(self):
# TODO move this logic into an Olm method
olm = self.server.olm
if olm.device_keys == self.keys:
return
olm.device_keys = self.keys
# TODO invalidate megolm sessions for rooms that got new devices
class MatrixKeyClaimEvent(MatrixEvent):
def __init__(self, server, room_id, keys):
self.keys = keys
self.room_id = room_id
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
W.prnt("", pprint.pformat(parsed_dict))
keys = []
try:
for user_id, user_dict in parsed_dict["one_time_keys"].items():
for device_id, device_dict in user_dict.items():
for key_dict in device_dict.values():
# TODO check the signature of the key
key = OneTimeKey(user_id, device_id, key_dict["key"])
keys.append(key)
return cls(server, room_id, keys)
except KeyError:
return MatrixErrorEvent.from_dict(
server, ("Error claiming onetime keys."), False, parsed_dict)
def execute(self):
server = self.server
olm = server.olm
for key in self.keys:
olm.create_session(key.user_id, key.device_id, key.key)
while server.encryption_queue[self.room_id]:
formatted_message = server.encryption_queue[self.room_id].popleft()
room, _ = server.find_room_from_id(self.room_id)
server.send_room_message(room, formatted_message, True)
class MatrixToDeviceEvent(MatrixEvent):
def __init__(self, server):
MatrixEvent.__init__(self, server)
@classmethod
def from_dict(cls, server, parsed_dict):
try:
if parsed_dict == {}:
return cls(server)
raise KeyError
except KeyError:
return MatrixErrorEvent.from_dict(server, ("Error sending to "
"device message"),
False, parsed_dict)
class MatrixBacklogEvent(MatrixEvent):
def __init__(self, server, room_id, end_token, events):
self.room_id = room_id
self.end_token = end_token
self.events = events
MatrixEvent.__init__(self, server)
@staticmethod
def _room_event_from_dict(room_id, event_dict):
if room_id != event_dict["room_id"]:
raise ValueError
if "redacted_by" in event_dict["unsigned"]:
return RoomRedactedMessageEvent.from_dict(event_dict)
return RoomMessageEvent.from_dict(event_dict)
@classmethod
def from_dict(cls, server, room_id, parsed_dict):
try:
end_token = sanitize_id(parsed_dict["end"])
if not parsed_dict["chunk"]:
return cls(server, room_id, end_token, [])
event_func = partial(MatrixBacklogEvent._room_event_from_dict,
room_id)
message_events = list(
filter(lambda event: event["type"] == "m.room.message",
parsed_dict["chunk"]))
events = [event_func(m) for m in message_events]
return cls(server, room_id, end_token, events)
except (KeyError, ValueError, TypeError):
return MatrixErrorEvent.from_dict(server, "Error fetching backlog",
False, parsed_dict)

View file

@ -1,93 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright © 2018 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals
from builtins import str
import json
from enum import Enum, unique
@unique
class RequestType(Enum):
GET = 0
POST = 1
PUT = 2
class HttpResponse:
def __init__(self, status, headers, body):
self.status = status # type: int
self.headers = headers # type: Dict[str, str]
self.body = body # type: bytes
# yapf: disable
class HttpRequest:
def __init__(
self,
request_type, # type: RequestType
host, # type: str
location, # type: str
data=None, # type: Dict[str, Any]
user_agent='weechat-matrix/{version}'.format(
version="0.1") # type: str
):
# type: (...) -> None
user_agent = 'User-Agent: {agent}'.format(agent=user_agent)
host_header = 'Host: {host}'.format(host=host)
keepalive = "Connection: keep-alive"
request_list = [] # type: List[str]
accept_header = 'Accept: */*' # type: str
end_separator = '\r\n' # type: str
payload = "" # type: str
# yapf: enable
if request_type == RequestType.GET:
get = 'GET {location} HTTP/1.1'.format(location=location)
request_list = [
get, host_header, user_agent, keepalive, accept_header,
end_separator
]
elif (request_type == RequestType.POST or
request_type == RequestType.PUT):
json_data = json.dumps(data, separators=(',', ':'))
if request_type == RequestType.POST:
method = "POST"
else:
method = "PUT"
request_line = '{method} {location} HTTP/1.1'.format(
method=method, location=location)
type_header = 'Content-Type: application/x-www-form-urlencoded'
length_header = 'Content-Length: {length}'.format(
length=len(json_data))
request_list = [
request_line, host_header, user_agent, keepalive,
accept_header, length_header, type_header, end_separator
]
payload = json_data
request = '\r\n'.join(request_list)
self.request = request
self.payload = payload

View file

@ -15,55 +15,23 @@
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from __future__ import unicode_literals from __future__ import unicode_literals
from builtins import str, bytes
import os import os
import ssl import ssl
import socket import socket
import time import time
import datetime
import pprint import pprint
import json
from collections import deque, defaultdict from collections import deque, defaultdict
from http_parser.pyparser import HttpParser
from nio import Client, LoginResponse, SyncRepsponse from nio import HttpClient, LoginResponse, SyncRepsponse
from matrix.plugin_options import Option, DebugType from matrix.plugin_options import Option, DebugType
from matrix.utils import (key_from_value, prnt_debug, server_buffer_prnt, from matrix.utils import (key_from_value, prnt_debug, server_buffer_prnt,
create_server_buffer, tags_for_message, create_server_buffer)
server_ts_to_weechat, shorten_sender)
from matrix.utf import utf8_decode from matrix.utf import utf8_decode
from matrix.globals import W, SERVERS, OPTIONS from matrix.globals import W, SERVERS
import matrix.api as API
from .buffer import RoomBuffer, OwnMessage, OwnAction from .buffer import RoomBuffer, OwnMessage, OwnAction
from matrix.api import (
MatrixClient,
MatrixSyncMessage,
MatrixLoginMessage,
MatrixKeyUploadMessage,
MatrixKeyQueryMessage,
MatrixToDeviceMessage,
MatrixSendMessage,
MatrixEncryptedMessage,
MatrixKeyClaimMessage
)
from .events import (
MatrixSendEvent,
MatrixBacklogEvent,
MatrixErrorEvent,
MatrixEmoteEvent,
MatrixJoinEvent
)
from matrix.encryption import (
Olm,
EncryptionError,
OlmTrustError,
encrypt_enabled
)
try: try:
FileNotFoundError FileNotFoundError
@ -108,7 +76,6 @@ class MatrixServer:
self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext self.ssl_context = ssl.create_default_context() # type: ssl.SSLContext
self.client = None self.client = None
self.nio_client = Client() # type: Option[Client]
self.access_token = None # type: str self.access_token = None # type: str
self.next_batch = None # type: str self.next_batch = None # type: str
self.transaction_id = 0 # type: int self.transaction_id = 0 # type: int
@ -117,18 +84,8 @@ class MatrixServer:
self.send_fd_hook = None # type: weechat.hook self.send_fd_hook = None # type: weechat.hook
self.send_buffer = b"" # type: bytes self.send_buffer = b"" # type: bytes
self.current_message = None # type: MatrixMessage
self.device_check_timestamp = None self.device_check_timestamp = None
self.http_parser = HttpParser() # type: HttpParser
self.http_buffer = [] # type: List[bytes]
# Queue of messages we need to send off.
self.send_queue = deque() # type: Deque[MatrixMessage]
# Queue of messages we send off and are waiting a response for
self.receive_queue = deque() # type: Deque[MatrixMessage]
self.event_queue_timer = None self.event_queue_timer = None
self.event_queue = deque() # type: Deque[RoomInfo] self.event_queue = deque() # type: Deque[RoomInfo]
@ -166,48 +123,6 @@ class MatrixServer:
with open(path, 'w') as f: with open(path, 'w') as f:
f.write(self.device_id) f.write(self.device_id)
def _load_olm(self):
try:
self.olm = Olm.from_session_dir(
self.user,
self.device_id,
self.get_session_path()
)
message = ("{prefix}matrix: Loaded Olm account for {user} (device:"
"{device})").format(prefix=W.prefix("network"),
user=self.user,
device=self.device_id)
W.prnt("", message)
except FileNotFoundError:
pass
except EncryptionError as error:
message = ("{prefix}matrix: Error loading Olm"
"account: {error}.").format(
prefix=W.prefix("error"), error=error)
W.prnt("", message)
@encrypt_enabled
def create_olm(self):
message = ("{prefix}matrix: Creating new Olm identity for "
"{self_color}{user}{ncolor}"
" on {server_color}{server}{ncolor} for device "
"{device}.").format(
prefix=W.prefix("network"),
self_color=W.color("chat_nick_self"),
ncolor=W.color("reset"),
user=self.user_id,
server_color=W.color("chat_server"),
server=self.name,
device=self.device_id)
W.prnt(self.server_buffer, message)
self.olm = Olm(self.user, self.device_id, self.get_session_path())
@encrypt_enabled
def store_olm(self):
if self.olm:
self.olm.to_session_dir()
def _create_options(self, config_file): def _create_options(self, config_file):
options = [ options = [
Option('autoconnect', 'boolean', '', 0, 0, 'off', Option('autoconnect', 'boolean', '', 0, 0, 'off',
@ -243,15 +158,9 @@ class MatrixServer:
option.max, option.value, option.value, 0, "", "", option.max, option.value, option.value, 0, "", "",
"matrix_config_server_change_cb", self.name, "", "") "matrix_config_server_change_cb", self.name, "", "")
def reset_parser(self):
self.http_parser = HttpParser()
self.http_buffer = []
def _change_client(self): def _change_client(self):
host = ':'.join([self.address, str(self.port)]) host = ':'.join([self.address, str(self.port)])
user_agent = 'weechat-matrix/{version}'.format(version="0.1") self.client = HttpClient(host, self.user)
self.client = MatrixClient(host, user_agent=user_agent)
# self.nio_client = Client()
def update_option(self, option, option_name): def update_option(self, option, option_name):
if option_name == "address": if option_name == "address":
@ -280,13 +189,9 @@ class MatrixServer:
value = W.config_string(option) value = W.config_string(option)
self.user = value self.user = value
self.access_token = "" self.access_token = ""
self.nio_client.user = value
self.nio_client.access_token = ""
self._load_device_id() if self.client:
self.client.user = value
# if self.device_id:
# self._load_olm()
elif option_name == "password": elif option_name == "password":
value = W.config_string(option) value = W.config_string(option)
@ -305,7 +210,6 @@ class MatrixServer:
"Adding to queue").format( "Adding to queue").format(
prefix=W.prefix("error"), prefix=W.prefix("error"),
t=message.__class__.__name__)) t=message.__class__.__name__))
self.send_queue.append(message)
def try_send(self, message): def try_send(self, message):
# type: (MatrixServer, bytes) -> bool # type: (MatrixServer, bytes) -> bool
@ -362,37 +266,22 @@ class MatrixServer:
return True return True
def _abort_send(self): def _abort_send(self):
self.send_queue.appendleft(self.current_message)
self.current_message = None self.current_message = None
self.send_buffer = "" self.send_buffer = ""
def _finalize_send(self): def _finalize_send(self):
# type: (MatrixServer) -> None # type: (MatrixServer) -> None
self.current_message.send_time = time.time()
self.receive_queue.append(self.current_message)
self.send_buffer = b"" self.send_buffer = b""
self.current_message = None
def send(self, message): def send(self, data):
# type: (MatrixServer, MatrixMessage) -> bool # type: (bytes) -> bool
if self.current_message: self.try_send(data)
return False
self.current_message = message
request = message.request.request
payload = message.request.payload
bytes_message = bytes(request, 'utf-8') + bytes(payload, 'utf-8')
self.try_send(bytes_message)
return True return True
def reconnect(self): def reconnect(self):
message = ("{prefix}matrix: reconnecting to server..." message = ("{prefix}matrix: reconnecting to server..."
).format(prefix=W.prefix("network")) ).format(prefix=W.prefix("network"))
server_buffer_prnt(self, message) server_buffer_prnt(self, message)
@ -437,8 +326,6 @@ class MatrixServer:
self.socket = None self.socket = None
self.connected = False self.connected = False
self.access_token = "" self.access_token = ""
self.send_queue.clear()
self.receive_queue.clear()
self.send_buffer = b"" self.send_buffer = b""
self.current_message = None self.current_message = None
@ -500,124 +387,13 @@ class MatrixServer:
return True return True
def sync(self): def sync(self):
limit = None if self.next_batch else OPTIONS.sync_limit request = self.client.sync()
message = MatrixSyncMessage(self.client, self.next_batch, limit) self.send_or_queue(request)
self.send_queue.append(message)
def _send_unencrypted_message(self, room_id, formatted_data):
message = MatrixSendMessage(
self.client, room_id=room_id, formatted_message=formatted_data)
self.send_or_queue(message)
def send_room_message(
self,
room,
formatted_data,
already_claimed=False
):
# type: (str, Formatted) -> None
if not room.encrypted:
self._send_unencrypted_message(room.room_id, formatted_data)
return
# TODO don't send messages unless all the devices are verified
missing = self.olm.get_missing_sessions(room.users.keys())
if missing and not already_claimed:
W.prnt("", "{prefix}matrix: Olm session missing for room, can't"
" encrypt message.")
W.prnt("", pprint.pformat(missing))
self.encryption_queue[room.room_id].append(formatted_data)
message = MatrixKeyClaimMessage(self.client, room.room_id, missing)
self.send_or_queue(message)
return
body = {"msgtype": "m.text", "body": formatted_data.to_plain()}
if formatted_data.is_formatted():
body["format"] = "org.matrix.custom.html"
body["formatted_body"] = formatted_data.to_html()
plaintext_dict = {
"type": "m.room.message",
"content": body
}
W.prnt("", "matrix: Encrypting message")
try:
payload_dict, to_device_dict = self.olm.group_encrypt(
room.room_id,
plaintext_dict,
self.user_id,
room.users.keys()
)
if to_device_dict:
W.prnt("", "matrix: Megolm session missing for room.")
message = MatrixToDeviceMessage(self.client, to_device_dict)
self.send_queue.append(message)
message = MatrixEncryptedMessage(
self.client,
room.room_id,
formatted_data,
payload_dict
)
self.send_queue.append(message)
except OlmTrustError:
m = ("{prefix}matrix: Untrusted devices found in room, "
"verification is needed before sending a message").format(
prefix=W.prefix("error"))
W.prnt(self.server_buffer, m)
return
@encrypt_enabled
def upload_keys(self, device_keys=False, one_time_keys=False):
keys = self.olm.account.identity_keys if device_keys else None
one_time_keys = (self.olm.account.one_time_keys["curve25519"] if
one_time_keys else None)
message = MatrixKeyUploadMessage(self.client, self.user_id,
self.device_id, self.olm,
keys, one_time_keys)
self.send_queue.append(message)
@encrypt_enabled
def check_one_time_keys(self, key_count):
max_keys = self.olm.account.max_one_time_keys
key_count = (max_keys / 2) - key_count
if key_count <= 0:
return
self.olm.account.generate_one_time_keys(key_count)
self.upload_keys(device_keys=False, one_time_keys=True)
@encrypt_enabled
def query_keys(self):
users = []
for room_buffer in self.room_buffers.values():
if not room_buffer.room.encrypted:
continue
users += list(room_buffer.room.users)
if not users:
return
message = MatrixKeyQueryMessage(self.client, users)
self.send_queue.append(message)
def login(self): def login(self):
# type: (MatrixServer) -> None # type: () -> None
message = MatrixLoginMessage(self.client, self.user, self.password, request = self.client.login(self.password)
self.device_name, self.device_id) self.send_or_queue(request)
self.send_or_queue(message)
msg = "{prefix}matrix: Logging in...".format(prefix=W.prefix("network")) msg = "{prefix}matrix: Logging in...".format(prefix=W.prefix("network"))
@ -698,92 +474,18 @@ class MatrixServer:
# self.check_one_time_keys(response.one_time_key_count) # self.check_one_time_keys(response.one_time_key_count)
# self.handle_events() # self.handle_events()
def handle_matrix_response(self, response): def handle_response(self, response):
if isinstance(response, MatrixSendEvent): # type: (MatrixMessage) -> None
room_buffer = self.find_room_from_id(response.room_id)
self.handle_own_messages(room_buffer, response.message)
elif isinstance(response, MatrixBacklogEvent):
room_buffer = self.find_room_from_id(response.room_id)
room_buffer.handle_backlog(response.events)
W.bar_item_update("buffer_modes")
elif isinstance(response, MatrixErrorEvent):
self._handle_erorr_response(response)
def nio_receive(self):
response = self.nio_client.next_response()
if isinstance(response, LoginResponse): if isinstance(response, LoginResponse):
self._handle_login(response) self._handle_login(response)
elif isinstance(response, SyncRepsponse): elif isinstance(response, SyncRepsponse):
self._handle_sync(response) self._handle_sync(response)
def nio_parse_response(self, response):
if isinstance(response, MatrixLoginMessage):
self.nio_client.receive("login", response.response.body)
elif isinstance(response, MatrixSyncMessage):
self.nio_client.receive("sync", response.response.body)
self.nio_receive()
return
def handle_response(self, message):
# type: (MatrixMessage) -> None
assert message.response
if ('content-type' in message.response.headers and
message.response.headers['content-type'] == 'application/json'):
if isinstance(message, (MatrixLoginMessage, MatrixSyncMessage)):
self.nio_parse_response(message)
else:
ret, error = message.decode_body(self)
if not ret:
message = ("{prefix}matrix: Error decoding json response"
" from server: {error}").format(
prefix=W.prefix("error"), error=error)
W.prnt(self.server_buffer, message)
return
event = message.event
self.handle_matrix_response(event)
else:
status_code = message.response.status
if status_code == 504:
if isinstance(message, API.MatrixSyncMessage):
self.sync()
else:
self._print_message_error(message)
else:
self._print_message_error(message)
creation_date = datetime.datetime.fromtimestamp(message.creation_time)
done_time = time.time()
info_message = (
"Message of type {t} created at {c}."
"\nMessage lifetime information:"
"\n Send delay: {s} ms"
"\n Receive delay: {r} ms"
"\n Handling time: {h} ms"
"\n Total time: {total} ms").format(
t=message.__class__.__name__,
c=creation_date,
s=(message.send_time - message.creation_time) * 1000,
r=(message.receive_time - message.send_time) * 1000,
h=(done_time - message.receive_time) * 1000,
total=(done_time - message.creation_time) * 1000,
)
prnt_debug(DebugType.TIMING, self, info_message)
return return
def create_room_buffer(self, room_id): def create_room_buffer(self, room_id):
room = self.nio_client.rooms[room_id] room = self.client.rooms[room_id]
buf = RoomBuffer(room, self.name) buf = RoomBuffer(room, self.name)
# TODO this should turned into a propper class # TODO this should turned into a propper class
self.room_buffers[room_id] = buf self.room_buffers[room_id] = buf
@ -867,31 +569,31 @@ def matrix_timer_cb(server_name, remaining_calls):
if not server.connected: if not server.connected:
return W.WEECHAT_RC_OK return W.WEECHAT_RC_OK
# check lag, disconnect if it's too big # # check lag, disconnect if it's too big
if server.receive_queue: # if server.receive_queue:
message = server.receive_queue.popleft() # message = server.receive_queue.popleft()
server.lag = (current_time - message.send_time) * 1000 # server.lag = (current_time - message.send_time) * 1000
server.receive_queue.appendleft(message) # server.receive_queue.appendleft(message)
server.lag_done = False # server.lag_done = False
W.bar_item_update("lag") # W.bar_item_update("lag")
# TODO print out message, make timeout configurable # # TODO print out message, make timeout configurable
if server.lag > 300000: # if server.lag > 300000:
server.disconnect() # server.disconnect()
return W.WEECHAT_RC_OK # return W.WEECHAT_RC_OK
while server.send_queue: # while server.send_queue:
message = server.send_queue.popleft() # message = server.send_queue.popleft()
prnt_debug( # prnt_debug(
DebugType.MESSAGING, # DebugType.MESSAGING,
server, ("Timer hook found message of type {t} in queue. Sending " # server, ("Timer hook found message of type {t} in queue. Sending "
"out.".format(t=message.__class__.__name__))) # "out.".format(t=message.__class__.__name__)))
if not server.send(message): # if not server.send(message):
# We got an error while sending the last message return the message # # We got an error while sending the last message return the message
# to the queue and exit the loop # # to the queue and exit the loop
server.send_queue.appendleft(message) # server.send_queue.appendleft(message)
break # break
if not server.next_batch: if not server.next_batch:
return W.WEECHAT_RC_OK return W.WEECHAT_RC_OK
@ -904,7 +606,6 @@ def matrix_timer_cb(server_name, remaining_calls):
"{prefix}matrix: Querying user devices.".format( "{prefix}matrix: Querying user devices.".format(
prefix=W.prefix("networ"))) prefix=W.prefix("networ")))
server.query_keys()
server.device_check_timestamp = current_time server.device_check_timestamp = current_time
return W.WEECHAT_RC_OK return W.WEECHAT_RC_OK