# -*- coding: utf-8 -*- # Weechat Matrix Protocol Script # Copyright © 2018 Damir Jelić # # 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 socket import ssl import time import pprint # pylint: disable=redefined-builtin from builtins import str # pylint: disable=unused-import from typing import (List, Set, Dict, Tuple, Text, Optional, AnyStr, Deque, Any) from matrix.colors import Formatted from matrix.utf import utf8_decode from matrix.http import HttpResponse from matrix.api import ( MessageType, MatrixSendMessage ) from matrix.messages import handle_http_response # Weechat searches for the registered callbacks in the scope of the main script # file, import the callbacks here so weechat can find them. from matrix.commands import ( hook_commands, hook_page_up, matrix_command_cb, matrix_command_join_cb, matrix_command_part_cb, matrix_command_invite_cb, matrix_command_topic_cb, matrix_command_pgup_cb, matrix_redact_command_cb, matrix_command_buf_clear_cb ) from matrix.server import ( MatrixServer, create_default_server, send_cb, matrix_timer_cb, matrix_config_server_read_cb, matrix_config_server_write_cb, matrix_config_server_change_cb, ) from matrix.bar_items import ( init_bar_items, matrix_bar_item_name, matrix_bar_item_plugin, matrix_bar_item_lag ) from matrix.completion import ( init_completion, matrix_command_completion_cb, matrix_server_command_completion_cb, matrix_debug_completion_cb, matrix_message_completion_cb, matrix_server_completion_cb ) from matrix.utils import ( key_from_value, server_buffer_prnt, prnt_debug, tags_from_line_data, server_buffer_set_title ) from matrix.plugin_options import ( DebugType, RedactType, ServerBufferType, ) from matrix.config import ( matrix_config_init, matrix_config_read, matrix_config_free, matrix_config_change_cb, matrix_config_reload_cb ) import matrix.globals from matrix.globals import W, OPTIONS, SERVERS WEECHAT_SCRIPT_NAME = "matrix" # type: str WEECHAT_SCRIPT_DESCRIPTION = "matrix chat plugin" # type: str WEECHAT_SCRIPT_AUTHOR = "Damir Jelić " # type: str WEECHAT_SCRIPT_VERSION = "0.1" # type: str WEECHAT_SCRIPT_LICENSE = "ISC" # type: str def wrap_socket(server, file_descriptor): # type: (MatrixServer, int) -> None sock = None # type: socket.socket temp_socket = socket.fromfd( file_descriptor, socket.AF_INET, socket.SOCK_STREAM ) # 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 # pylint: disable=protected-access,unidiomatic-typecheck if type(temp_socket) == socket._socket.socket: # pylint: disable=no-member sock = socket._socketobject(_sock=temp_socket) else: sock = temp_socket # 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 sock.setblocking(False) message = "{prefix}matrix: Doing SSL handshake...".format( prefix=W.prefix("network")) W.prnt(server.server_buffer, message) ssl_socket = server.ssl_context.wrap_socket( sock, do_handshake_on_connect=False, server_hostname=server.address) # type: ssl.SSLSocket server.socket = ssl_socket try_ssl_handshake(server) @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): sock = server.socket while True: try: sock.do_handshake() cipher = sock.cipher() cipher_message = ("{prefix}matrix: Connected using {tls}, and " "{bit} bit {cipher} cipher suite.").format( prefix=W.prefix("network"), tls=cipher[1], bit=cipher[2], cipher=cipher[0]) W.prnt(server.server_buffer, cipher_message) # TODO print out the certificates # cert = sock.getpeercert() # W.prnt(server.server_buffer, pprint.pformat(cert)) finalize_connection(server) return True except ssl.SSLWantReadError: hook = W.hook_fd( server.socket.fileno(), 1, 0, 0, "ssl_fd_cb", server.name ) server.ssl_hook = hook return False except ssl.SSLWantWriteError: hook = W.hook_fd( server.socket.fileno(), 0, 1, 0, "ssl_fd_cb", server.name ) server.ssl_hook = hook return False except ssl.SSLError as error: str_error = error.reason if error.reason else "Unknown error" message = ("{prefix}Error while doing SSL handshake" ": {error}").format( prefix=W.prefix("network"), error=str_error) server_buffer_prnt(server, message) server_buffer_prnt( server, ("{prefix}matrix: disconnecting from server...").format( prefix=W.prefix("network"))) server.disconnect() return False @utf8_decode def receive_cb(server_name, file_descriptor): server = SERVERS[server_name] while True: try: data = server.socket.recv(4096) except ssl.SSLWantReadError: break 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( prefix=W.prefix("network"), error=str_error) server_buffer_prnt(server, message) server_buffer_prnt( server, ("{prefix}matrix: disconnecting from server...").format( prefix=W.prefix("network"))) server.disconnect() # Queue the failed message for resending if server.receive_queue: message = server.receive_queue.popleft() server.send_queue.appendleft(message) return W.WEECHAT_RC_OK if not data: server_buffer_prnt( server, "{prefix}matrix: Error while reading from socket".format( prefix=W.prefix("network"))) server_buffer_prnt( server, ("{prefix}matrix: disconnecting from server...").format( prefix=W.prefix("network"))) # Queue the failed message for resending if server.receive_queue: message = server.receive_queue.popleft() server.send_queue.appendleft(message) server.disconnect() break received = len(data) # type: int parsed_bytes = server.http_parser.execute(data, received) assert parsed_bytes == received if server.http_parser.is_partial_body(): server.http_buffer.append(server.http_parser.recv_body()) 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.type, s=status)) # Message done, reset the parser state. server.reset_parser() handle_http_response(server, message) break return W.WEECHAT_RC_OK def finalize_connection(server): hook = W.hook_fd( server.socket.fileno(), 1, 0, 0, "receive_cb", server.name ) server.fd_hook = hook server.connected = True server.connecting = False server.login() @utf8_decode def connect_cb(data, status, gnutls_rc, sock, error, ip_address): # pylint: disable=too-many-arguments,too-many-branches status_value = int(status) # type: int server = SERVERS[data] if status_value == W.WEECHAT_HOOK_CONNECT_OK: file_descriptor = int(sock) # type: int server.numeric_address = ip_address server_buffer_set_title(server) wrap_socket(server, file_descriptor) return W.WEECHAT_RC_OK elif status_value == W.WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND: W.prnt( server.server_buffer, '{address} not found'.format(address=ip_address) ) elif status_value == W.WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND: W.prnt(server.server_buffer, 'IP address not found') elif status_value == W.WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED: W.prnt(server.server_buffer, 'Connection refused') elif status_value == W.WEECHAT_HOOK_CONNECT_PROXY_ERROR: W.prnt( server.server_buffer, 'Proxy fails to establish connection to server' ) elif status_value == W.WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR: W.prnt(server.server_buffer, 'Unable to set local hostname') elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR: W.prnt(server.server_buffer, 'TLS init error') elif status_value == W.WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR: W.prnt(server.server_buffer, 'TLS Handshake failed') elif status_value == W.WEECHAT_HOOK_CONNECT_MEMORY_ERROR: W.prnt(server.server_buffer, 'Not enough memory') elif status_value == W.WEECHAT_HOOK_CONNECT_TIMEOUT: W.prnt(server.server_buffer, 'Timeout') elif status_value == W.WEECHAT_HOOK_CONNECT_SOCKET_ERROR: W.prnt(server.server_buffer, 'Unable to create socket') else: W.prnt( server.server_buffer, 'Unexpected error: {status}'.format(status=status_value) ) server.schedule_reconnect() return W.WEECHAT_RC_OK @utf8_decode def room_input_cb(server_name, buffer, input_data): server = SERVERS[server_name] if not server.connected: message = "{prefix}matrix: you are not connected to the server".format( prefix=W.prefix("error")) W.prnt(buffer, message) return W.WEECHAT_RC_ERROR room_id = key_from_value(server.buffers, buffer) room = server.rooms[room_id] if room.encrypted: return W.WEECHAT_RC_OK formatted_data = Formatted.from_input_line(input_data) message = MatrixSendMessage( server.client, room_id=room_id, formatted_message=formatted_data ) server.send_or_queue(message) return W.WEECHAT_RC_OK @utf8_decode def room_close_cb(data, buffer): W.prnt("", "Buffer '%s' will be closed!" % W.buffer_get_string(buffer, "name")) return W.WEECHAT_RC_OK @utf8_decode def matrix_unload_cb(): matrix_config_free(matrix.globals.CONFIG) return W.WEECHAT_RC_OK def autoconnect(servers): for server in servers.values(): if server.autoconnect: server.connect() if __name__ == "__main__": if W.register(WEECHAT_SCRIPT_NAME, WEECHAT_SCRIPT_AUTHOR, WEECHAT_SCRIPT_VERSION, WEECHAT_SCRIPT_LICENSE, WEECHAT_SCRIPT_DESCRIPTION, 'matrix_unload_cb', ''): # TODO if this fails we should abort and unload the script. matrix.globals.CONFIG = W.config_new( "matrix", "matrix_config_reload_cb", "" ) matrix_config_init(matrix.globals.CONFIG) matrix_config_read(matrix.globals.CONFIG) hook_commands() init_bar_items() init_completion() if not SERVERS: create_default_server(matrix.globals.CONFIG) autoconnect(SERVERS)