From 66897c15499bac615211cf7186a072e75336c0e6 Mon Sep 17 00:00:00 2001 From: Barbeque Sauce Date: Mon, 2 Dec 2019 13:30:27 -0500 Subject: [PATCH] IMP: Updated deluge_client for v2 compatibility --- lib/deluge_client/__init__.py | 2 +- lib/deluge_client/client.py | 252 ++++++++++++++++++++++----- lib/deluge_client/rencode.py | 310 +++++++++++++++++++--------------- lib/deluge_client/tests.py | 96 +++++++---- 4 files changed, 443 insertions(+), 217 deletions(-) diff --git a/lib/deluge_client/__init__.py b/lib/deluge_client/__init__.py index e70f458d..125e0d16 100644 --- a/lib/deluge_client/__init__.py +++ b/lib/deluge_client/__init__.py @@ -1 +1 @@ -from .client import DelugeRPCClient \ No newline at end of file +from .client import DelugeRPCClient, FailedToReconnectException \ No newline at end of file diff --git a/lib/deluge_client/client.py b/lib/deluge_client/client.py index 59695da2..827fcd7c 100644 --- a/lib/deluge_client/client.py +++ b/lib/deluge_client/client.py @@ -2,6 +2,7 @@ import logging import socket import ssl import struct +import warnings import zlib from .rencode import dumps, loads @@ -10,102 +11,265 @@ RPC_RESPONSE = 1 RPC_ERROR = 2 RPC_EVENT = 3 -#MESSAGE_HEADER_SIZE = 5 +MESSAGE_HEADER_SIZE = 5 READ_SIZE = 10 logger = logging.getLogger(__name__) -class ConnectionLostException(Exception): + +class DelugeClientException(Exception): + """Base exception for all deluge client exceptions""" + + +class ConnectionLostException(DelugeClientException): pass -class CallTimeoutException(Exception): + +class CallTimeoutException(DelugeClientException): pass + +class InvalidHeaderException(DelugeClientException): + pass + + +class FailedToReconnectException(DelugeClientException): + pass + + +class RemoteException(DelugeClientException): + pass + + class DelugeRPCClient(object): timeout = 20 - - def __init__(self, host, port, username, password): + + def __init__(self, host, port, username, password, decode_utf8=False, automatic_reconnect=True): self.host = host self.port = port self.username = username self.password = password - + self.deluge_version = None + # This is only applicable if deluge_version is 2 + self.deluge_protocol_version = None + + self.decode_utf8 = decode_utf8 + if not self.decode_utf8: + warnings.warn('Using `decode_utf8=False` is deprecated, please set it to True.' + 'The argument will be removed in a future release where it will be always True', DeprecationWarning) + + self.automatic_reconnect = automatic_reconnect + self.request_id = 1 self.connected = False self._create_socket() - + def _create_socket(self, ssl_version=None): if ssl_version is not None: self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), ssl_version=ssl_version) else: self._socket = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) self._socket.settimeout(self.timeout) - + def connect(self): """ Connects to the Deluge instance """ + self._connect() + logger.debug('Connected to Deluge, detecting daemon version') + self._detect_deluge_version() + logger.debug('Daemon version {} detected, logging in'.format(self.deluge_version)) + if self.deluge_version == 2: + result = self.call('daemon.login', self.username, self.password, client_version='deluge-client') + else: + result = self.call('daemon.login', self.username, self.password) + logger.debug('Logged in with value %r' % result) + self.connected = True + + def _connect(self): logger.info('Connecting to %s:%s' % (self.host, self.port)) try: self._socket.connect((self.host, self.port)) except ssl.SSLError as e: - if e.reason != 'UNSUPPORTED_PROTOCOL' or not hasattr(ssl, 'PROTOCOL_SSLv3'): + # Note: have not verified that we actually get errno 258 for this error + if (hasattr(ssl, 'PROTOCOL_SSLv3') and + (getattr(e, 'reason', None) == 'UNSUPPORTED_PROTOCOL' or e.errno == 258)): + logger.warning('Was unable to ssl handshake, trying to force SSLv3 (insecure)') + self._create_socket(ssl_version=ssl.PROTOCOL_SSLv3) + self._socket.connect((self.host, self.port)) + else: raise - - logger.warning('Was unable to ssl handshake, trying to force SSLv3 (insecure)') - self._create_socket(ssl_version=ssl.PROTOCOL_SSLv3) - self._socket.connect((self.host, self.port)) - - logger.debug('Connected to Deluge, logging in') - result = self.call('daemon.login', self.username, self.password) - logger.debug('Logged in with value %r' % result) - self.connected = True - + def disconnect(self): """ Disconnect from deluge """ if self.connected: self._socket.close() - - def call(self, method, *args, **kwargs): - """ - Calls an RPC function - """ + self._socket = None + self.connected = False + + def _detect_deluge_version(self): + if self.deluge_version is not None: + return + + self._send_call(1, None, 'daemon.info') + self._send_call(2, None, 'daemon.info') + self._send_call(2, 1, 'daemon.info') + result = self._socket.recv(1) + if result[:1] == b'D': + # This is a protocol deluge 2.0 was using before release + self.deluge_version = 2 + self.deluge_protocol_version = None + # If we need the specific version of deluge 2, this is it. + daemon_version = self._receive_response(2, None, partial_data=result) + elif ord(result[:1]) == 1: + self.deluge_version = 2 + self.deluge_protocol_version = 1 + # If we need the specific version of deluge 2, this is it. + daemon_version = self._receive_response(2, 1, partial_data=result) + else: + self.deluge_version = 1 + # Deluge 1 doesn't recover well from the bad request. Re-connect the socket. + self._socket.close() + self._create_socket() + self._connect() + + def _send_call(self, deluge_version, protocol_version, method, *args, **kwargs): self.request_id += 1 - logger.debug('Calling reqid %s method %r with args:%r kwargs:%r' % (self.request_id, method, args, kwargs)) - + if method == 'daemon.login': + debug_args = list(args) + if len(debug_args) >= 2: + debug_args[1] = '