weechat-matrix/main.py

474 lines
16 KiB
Python
Raw Normal View History

2017-12-30 14:03:03 +01:00
# -*- 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.
2017-12-30 14:03:03 +01:00
from __future__ import unicode_literals
import socket
import ssl
2018-02-28 14:36:59 +01:00
import textwrap
2018-01-06 17:12:54 +01:00
# pylint: disable=redefined-builtin
from builtins import str
2018-08-29 19:40:59 +02:00
from itertools import chain
2018-01-06 17:12:54 +01:00
# pylint: disable=unused-import
2018-08-29 19:40:59 +02:00
from typing import Any, AnyStr, Deque, Dict, List, Optional, Set, Text, Tuple
2018-07-20 19:14:32 +02:00
2018-08-29 19:40:59 +02:00
import logbook
import OpenSSL.crypto as crypto
from future.utils import bytes_to_native_str as n
from logbook import Logger, StreamHandler
from nio import RemoteProtocolError, RemoteTransportError, TransportType
2018-08-29 19:40:59 +02:00
from matrix import globals as G
from matrix.bar_items import (init_bar_items, matrix_bar_item_buffer_modes,
matrix_bar_item_lag, matrix_bar_item_name,
matrix_bar_item_plugin)
from matrix.buffer import room_buffer_close_cb, room_buffer_input_cb
# Weechat searches for the registered callbacks in the scope of the main script
# file, import the callbacks here so weechat can find them.
2018-08-29 19:40:59 +02:00
from matrix.commands import (hook_commands, hook_page_up,
matrix_command_buf_clear_cb, matrix_command_cb,
matrix_command_pgup_cb, matrix_invite_command_cb,
matrix_join_command_cb, matrix_kick_command_cb,
matrix_me_command_cb, matrix_part_command_cb,
matrix_redact_command_cb, matrix_topic_command_cb)
from matrix.completion import (init_completion, matrix_command_completion_cb,
matrix_debug_completion_cb,
matrix_message_completion_cb,
matrix_olm_device_completion_cb,
matrix_olm_user_completion_cb,
matrix_server_command_completion_cb,
matrix_server_completion_cb,
matrix_user_completion_cb)
from matrix.config import (MatrixConfig, config_log_category_cb,
config_log_level_cb, config_server_buffer_cb,
matrix_config_reload_cb)
from matrix.globals import SCRIPT_NAME, SERVERS, W
from matrix.server import (MatrixServer, create_default_server,
matrix_config_server_change_cb,
matrix_config_server_read_cb,
matrix_config_server_write_cb, matrix_timer_cb,
send_cb)
from matrix.utf import utf8_decode
from matrix.utils import server_buffer_prnt, server_buffer_set_title
2018-02-21 17:00:11 +01:00
# yapf: disable
WEECHAT_SCRIPT_NAME = SCRIPT_NAME
2018-01-27 16:21:10 +01:00
WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str
WEECHAT_SCRIPT_AUTHOR = "Damir Jelić <poljar@termina.org.uk>" # type: str
WEECHAT_SCRIPT_VERSION = "0.1" # type: str
WEECHAT_SCRIPT_LICENSE = "ISC" # type: str
2018-02-21 17:00:11 +01:00
# yapf: enable
2017-12-30 14:03:03 +01:00
2018-07-21 19:54:36 +02:00
logger = Logger("matrix-cli")
2018-02-28 14:36:59 +01:00
def print_certificate_info(buff, sock, cert):
cert_pem = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
public_key = x509.get_pubkey()
key_type = ("RSA" if public_key.type() == crypto.TYPE_RSA else "DSA")
key_size = str(public_key.bits())
sha256_fingerprint = x509.digest(n(b"SHA256"))
sha1_fingerprint = x509.digest(n(b"SHA1"))
2018-02-28 14:36:59 +01:00
signature_algorithm = x509.get_signature_algorithm()
key_info = ("key info: {key_type} key {bits} bits, signed using "
"{algo}").format(
key_type=key_type, bits=key_size, algo=signature_algorithm)
validity_info = (" Begins on: {before}\n"
" Expires on: {after}").format(
before=cert["notBefore"], after=cert["notAfter"])
rdns = chain(*cert["subject"])
subject = ", ".join(["{}={}".format(name, value) for name, value in rdns])
rdns = chain(*cert["issuer"])
issuer = ", ".join(["{}={}".format(name, value) for name, value in rdns])
subject = "subject: {sub}, serial number {serial}".format(
sub=subject, serial=cert["serialNumber"])
issuer = "issuer: {issuer}".format(issuer=issuer)
fingerprints = (" SHA1: {}\n"
" SHA256: {}").format(sha1_fingerprint,
sha256_fingerprint)
wrapper = textwrap.TextWrapper(
initial_indent=" - ", subsequent_indent=" ")
message = ("{prefix}matrix: received certificate\n"
" - certificate info:\n"
"{subject}\n"
"{issuer}\n"
"{key_info}\n"
" - period of validity:\n{validity_info}\n"
" - fingerprints:\n{fingerprints}").format(
prefix=W.prefix("network"),
subject=wrapper.fill(subject),
issuer=wrapper.fill(issuer),
key_info=wrapper.fill(key_info),
validity_info=validity_info,
fingerprints=fingerprints)
W.prnt(buff, message)
2018-01-06 17:12:54 +01:00
def wrap_socket(server, file_descriptor):
2018-02-12 10:56:28 +01:00
# type: (MatrixServer, int) -> None
2018-01-06 17:12:54 +01:00
sock = None # type: socket.socket
2017-12-30 14:03:03 +01:00
2018-02-21 17:00:11 +01:00
temp_socket = socket.fromfd(file_descriptor, socket.AF_INET,
socket.SOCK_STREAM)
2018-01-03 12:14:24 +01:00
# For python 2.7 wrap_socket() doesn't work with sockets created from an
# file descriptor because fromfd() doesn't return a wrapped socket, the bug
# was fixed for python 3, more info: https://bugs.python.org/issue13942
2018-01-23 13:30:42 +01:00
# pylint: disable=protected-access,unidiomatic-typecheck
if type(temp_socket) == socket._socket.socket:
2018-01-07 15:46:18 +01:00
# pylint: disable=no-member
2018-01-06 17:12:54 +01:00
sock = socket._socketobject(_sock=temp_socket)
2017-12-30 14:03:03 +01:00
else:
2018-01-06 17:12:54 +01:00
sock = temp_socket
2017-12-30 14:03:03 +01:00
# fromfd() duplicates the file descriptor but doesn't retain it's blocking
# non-blocking attribute, so mark the socket as non-blocking even though
# weechat already did that for us
2018-01-30 19:29:03 +01:00
sock.setblocking(False)
2018-01-30 14:16:25 +01:00
2018-01-30 19:29:03 +01:00
message = "{prefix}matrix: Doing SSL handshake...".format(
prefix=W.prefix("network"))
W.prnt(server.server_buffer, message)
2018-01-30 14:16:25 +01:00
2018-01-30 19:29:03 +01:00
ssl_socket = server.ssl_context.wrap_socket(
2018-02-21 17:00:11 +01:00
sock, do_handshake_on_connect=False,
server_hostname=server.config.address) # type: ssl.SSLSocket
2018-01-06 17:12:54 +01:00
2018-01-30 19:29:03 +01:00
server.socket = ssl_socket
2018-01-30 14:16:25 +01:00
2018-01-30 19:29:03 +01:00
try_ssl_handshake(server)
2018-01-30 14:16:25 +01:00
2018-01-30 19:29:03 +01:00
@utf8_decode
def ssl_fd_cb(server_name, file_descriptor):
server = SERVERS[server_name]
if server.ssl_hook:
W.unhook(server.ssl_hook)
server.ssl_hook = None
try_ssl_handshake(server)
return W.WEECHAT_RC_OK
def try_ssl_handshake(server):
2018-02-01 18:02:58 +01:00
sock = server.socket
2018-01-30 19:29:03 +01:00
while True:
try:
2018-02-01 18:02:58 +01:00
sock.do_handshake()
2018-01-30 14:16:25 +01:00
2018-02-01 18:02:58 +01:00
cipher = sock.cipher()
2018-01-30 19:29:03 +01:00
cipher_message = ("{prefix}matrix: Connected using {tls}, and "
"{bit} bit {cipher} cipher suite.").format(
2018-02-01 18:02:58 +01:00
prefix=W.prefix("network"),
tls=cipher[1],
bit=cipher[2],
cipher=cipher[0])
2018-01-30 19:29:03 +01:00
W.prnt(server.server_buffer, cipher_message)
2018-02-28 14:36:59 +01:00
cert = sock.getpeercert()
if cert:
print_certificate_info(server.server_buffer, sock, cert)
2018-01-30 19:29:03 +01:00
finalize_connection(server)
return True
except ssl.SSLWantReadError:
2018-02-21 17:00:11 +01:00
hook = W.hook_fd(server.socket.fileno(), 1, 0, 0, "ssl_fd_cb",
server.name)
2018-01-30 19:29:03 +01:00
server.ssl_hook = hook
return False
except ssl.SSLWantWriteError:
2018-02-21 17:00:11 +01:00
hook = W.hook_fd(server.socket.fileno(), 0, 1, 0, "ssl_fd_cb",
server.name)
2018-01-30 19:29:03 +01:00
server.ssl_hook = hook
return False
except (ssl.SSLError, socket.error) as error:
try:
str_error = error.reason if error.reason else "Unknown error"
except AttributeError:
str_error = str(error)
message = ("{prefix}Error while doing SSL handshake"
": {error}").format(
2018-02-21 17:00:11 +01:00
prefix=W.prefix("network"), error=str_error)
server_buffer_prnt(server, message)
server_buffer_prnt(
2018-02-21 17:00:11 +01:00
server, ("{prefix}matrix: disconnecting from server..."
).format(prefix=W.prefix("network")))
server.disconnect()
2018-01-30 19:29:03 +01:00
return False
2017-12-30 14:03:03 +01:00
@utf8_decode
2018-01-06 17:12:54 +01:00
def receive_cb(server_name, file_descriptor):
server = SERVERS[server_name]
2017-12-30 14:03:03 +01:00
while True:
try:
data = server.socket.recv(4096)
2017-12-30 14:03:03 +01:00
except ssl.SSLWantReadError:
break
2018-01-06 17:12:54 +01:00
except socket.error as error:
errno = "error" + str(error.errno) + " " if error.errno else ""
str_error = error.strerror if error.strerror else "Unknown error"
str_error = errno + str_error
message = ("{prefix}Error while reading from "
"socket: {error}").format(
2018-02-21 17:00:11 +01:00
prefix=W.prefix("network"), error=str_error)
server_buffer_prnt(server, message)
server_buffer_prnt(
2018-02-21 17:00:11 +01:00
server, ("{prefix}matrix: disconnecting from server..."
2018-07-21 15:15:48 +02:00
).format(prefix=W.prefix("network")))
server.disconnect()
2017-12-30 14:03:03 +01:00
2018-01-06 17:12:54 +01:00
return W.WEECHAT_RC_OK
2017-12-30 14:03:03 +01:00
if not data:
server_buffer_prnt(
server,
"{prefix}matrix: Error while reading from socket".format(
prefix=W.prefix("network")))
server_buffer_prnt(
2018-02-21 17:00:11 +01:00
server, ("{prefix}matrix: disconnecting from server..."
2018-07-21 15:15:48 +02:00
).format(prefix=W.prefix("network")))
server.disconnect()
2017-12-30 14:03:03 +01:00
break
try:
server.client.receive(data)
2018-07-26 11:33:05 +02:00
except (RemoteTransportError, RemoteProtocolError) as e:
server.error(str(e))
server.disconnect()
break
2017-12-30 14:03:03 +01:00
2018-07-20 19:14:32 +02:00
response = server.client.next_response()
2017-12-30 14:03:03 +01:00
2018-07-21 15:15:48 +02:00
# Check if we need to send some data back
data_to_send = server.client.data_to_send()
if data_to_send:
server.send(data_to_send)
2018-07-20 19:14:32 +02:00
if response:
server.handle_response(response)
2017-12-30 14:03:03 +01:00
break
return W.WEECHAT_RC_OK
2018-01-30 19:29:03 +01:00
def finalize_connection(server):
2018-07-20 19:14:32 +02:00
hook = W.hook_fd(
server.socket.fileno(),
1,
0,
0,
"receive_cb",
server.name
)
2018-01-30 19:29:03 +01:00
server.fd_hook = hook
server.connected = True
server.connecting = False
2018-09-03 19:20:58 +02:00
server.reconnect_delay = 0
2018-07-21 15:15:48 +02:00
negotiated_protocol = server.socket.selected_alpn_protocol()
if negotiated_protocol is None:
negotiated_protocol = server.socket.selected_npn_protocol()
if negotiated_protocol == "http/1.1":
server.transport_type = TransportType.HTTP
2018-07-21 15:15:48 +02:00
elif negotiated_protocol == "h2":
server.transport_type = TransportType.HTTP2
2018-07-21 15:15:48 +02:00
data = server.client.connect(server.transport_type)
2018-07-21 15:15:48 +02:00
server.send(data)
2018-01-30 19:29:03 +01:00
server.login()
2018-01-30 19:29:03 +01:00
2017-12-30 14:03:03 +01:00
@utf8_decode
def connect_cb(data, status, gnutls_rc, sock, error, ip_address):
2018-01-07 16:04:17 +01:00
# pylint: disable=too-many-arguments,too-many-branches
2018-01-09 12:42:06 +01:00
status_value = int(status) # type: int
2018-01-03 12:14:24 +01:00
server = SERVERS[data]
2017-12-30 14:03:03 +01:00
if status_value == W.WEECHAT_HOOK_CONNECT_OK:
2018-01-06 17:12:54 +01:00
file_descriptor = int(sock) # type: int
2018-01-30 19:29:03 +01:00
server.numeric_address = ip_address
server_buffer_set_title(server)
2018-01-06 17:12:54 +01:00
2018-01-30 19:29:03 +01:00
wrap_socket(server, file_descriptor)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
W.prnt(
server.server_buffer,
2018-02-21 17:00:11 +01:00
'{address} not found'.format(address=ip_address))
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
W.prnt(server.server_buffer, 'IP address not found')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
W.prnt(server.server_buffer, 'Connection refused')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR:
2018-02-21 17:00:11 +01:00
W.prnt(server.server_buffer,
'Proxy fails to establish connection to server')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
W.prnt(server.server_buffer, 'Unable to set local hostname')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
W.prnt(server.server_buffer, 'TLS init error')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
W.prnt(server.server_buffer, 'TLS Handshake failed')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
W.prnt(server.server_buffer, 'Not enough memory')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT:
W.prnt(server.server_buffer, 'Timeout')
2017-12-30 14:03:03 +01:00
elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
W.prnt(server.server_buffer, 'Unable to create socket')
2017-12-30 14:03:03 +01:00
else:
W.prnt(
server.server_buffer,
2018-02-21 17:00:11 +01:00
'Unexpected error: {status}'.format(status=status_value))
2017-12-30 14:03:03 +01:00
server.disconnect(reconnect=True)
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
@utf8_decode
2018-01-03 12:14:24 +01:00
def room_close_cb(data, buffer):
2018-02-21 17:00:11 +01:00
W.prnt("",
"Buffer '%s' will be closed!" % W.buffer_get_string(buffer, "name"))
2017-12-30 14:03:03 +01:00
return W.WEECHAT_RC_OK
2018-01-03 12:14:24 +01:00
@utf8_decode
def matrix_unload_cb():
2018-08-29 15:35:36 +02:00
for server in SERVERS.values():
server.config.free()
G.CONFIG.free()
2018-07-20 19:14:32 +02:00
# for server in SERVERS.values():
# server.store_olm()
2018-01-03 12:14:24 +01:00
return W.WEECHAT_RC_OK
2018-01-06 17:12:54 +01:00
def autoconnect(servers):
for server in servers.values():
if server.config.autoconnect:
server.connect()
2018-01-06 17:12:54 +01:00
def debug_buffer_close_cb(data, buffer):
2018-08-29 15:35:36 +02:00
G.CONFIG.debug_buffer = ""
return W.WEECHAT_RC_OK
2018-07-21 19:54:36 +02:00
class WeechatHandler(StreamHandler):
def __init__(self, level=logbook.NOTSET, format_string=None, filter=None,
bubble=False):
StreamHandler.__init__(
self,
object(),
level,
format_string,
None,
filter,
bubble
)
def write(self, item):
buf = ""
2018-08-29 15:35:36 +02:00
if G.CONFIG.network.debug_buffer:
if not G.CONFIG.debug_buffer:
G.CONFIG.debug_buffer = W.buffer_new(
"Matrix Debug", "", "", "debug_buffer_close_cb", "")
2018-08-29 15:35:36 +02:00
buf = G.CONFIG.debug_buffer
W.prnt(buf, item)
2018-07-21 19:54:36 +02:00
2017-12-30 14:03:03 +01:00
if __name__ == "__main__":
2018-02-21 17:00:11 +01:00
if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR,
WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE,
WEECHAT_SCRIPT_DESCRIPTION, 'matrix_unload_cb', ''):
2017-12-30 14:03:03 +01:00
if not W.mkdir_home("matrix", 0o700):
message = ("{prefix}matrix: Error creating session "
"directory").format(prefix=W.prefix("error"))
W.prnt("", message)
2018-02-05 12:38:45 +01:00
2018-07-21 19:54:36 +02:00
handler = WeechatHandler()
handler.format_string = "{record.channel}: {record.message}"
handler.push_application()
# TODO if this fails we should abort and unload the script.
2018-08-29 15:35:36 +02:00
G.CONFIG = MatrixConfig()
G.CONFIG.read()
2018-01-03 12:14:24 +01:00
hook_commands()
2018-01-29 13:07:49 +01:00
init_bar_items()
2018-01-29 14:01:58 +01:00
init_completion()
2018-01-06 17:12:54 +01:00
if not SERVERS:
2018-08-29 15:35:36 +02:00
create_default_server(G.CONFIG)
2018-01-06 17:12:54 +01:00
autoconnect(SERVERS)