From aeb3e0fd6d6e01987fdfc33fc17a52631dd3706b Mon Sep 17 00:00:00 2001 From: Clinton Hall Date: Tue, 10 Dec 2019 12:55:13 +1300 Subject: [PATCH 1/3] Deluge update to V2 (#1683) Fixes #1680 --- core/plugins/downloaders/torrent/deluge.py | 6 +- core/utils/parsers.py | 2 +- libs/custom/deluge_client/__init__.py | 0 libs/custom/deluge_client/client.py | 275 ++++++++++++ libs/custom/deluge_client/rencode.py | 474 ++++++++++++++++++++ libs/custom/deluge_client/tests.py | 65 +++ libs/custom/synchronousdeluge/__init__.py | 31 -- libs/custom/synchronousdeluge/client.py | 172 ------- libs/custom/synchronousdeluge/exceptions.py | 18 - libs/custom/synchronousdeluge/protocol.py | 46 -- libs/custom/synchronousdeluge/transfer.py | 64 --- libs/requirements-custom.txt | 2 +- tox.ini | 2 +- 13 files changed, 820 insertions(+), 337 deletions(-) create mode 100644 libs/custom/deluge_client/__init__.py create mode 100644 libs/custom/deluge_client/client.py create mode 100644 libs/custom/deluge_client/rencode.py create mode 100644 libs/custom/deluge_client/tests.py delete mode 100644 libs/custom/synchronousdeluge/__init__.py delete mode 100644 libs/custom/synchronousdeluge/client.py delete mode 100644 libs/custom/synchronousdeluge/exceptions.py delete mode 100644 libs/custom/synchronousdeluge/protocol.py delete mode 100644 libs/custom/synchronousdeluge/transfer.py diff --git a/core/plugins/downloaders/torrent/deluge.py b/core/plugins/downloaders/torrent/deluge.py index 86542a8af..0c4c07c8c 100644 --- a/core/plugins/downloaders/torrent/deluge.py +++ b/core/plugins/downloaders/torrent/deluge.py @@ -5,7 +5,7 @@ unicode_literals, ) -from synchronousdeluge.client import DelugeClient +from deluge_client.client import DelugeRPCClient import core from core import logger @@ -19,9 +19,9 @@ def configure_client(): password = core.DELUGE_PASSWORD logger.debug('Connecting to {0}: http://{1}:{2}'.format(agent, host, port)) - client = DelugeClient() + client = DelugeRPCClient(host, port, user, password) try: - client.connect(host, port, user, password) + client.connect() except Exception: logger.error('Failed to connect to Deluge') else: diff --git a/core/utils/parsers.py b/core/utils/parsers.py index fd88ec638..eff4b3e95 100644 --- a/core/utils/parsers.py +++ b/core/utils/parsers.py @@ -65,7 +65,7 @@ def parse_deluge(args): input_hash = args[1] input_id = args[1] try: - input_category = core.TORRENT_CLASS.core.get_torrent_status(input_id, ['label']).get()['label'] + input_category = core.TORRENT_CLASS.core.get_torrent_status(input_id, ['label']).get(b'label').decode() except Exception: input_category = '' return input_directory, input_name, input_category, input_hash, input_id diff --git a/libs/custom/deluge_client/__init__.py b/libs/custom/deluge_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/custom/deluge_client/client.py b/libs/custom/deluge_client/client.py new file mode 100644 index 000000000..827fcd7c9 --- /dev/null +++ b/libs/custom/deluge_client/client.py @@ -0,0 +1,275 @@ +import logging +import socket +import ssl +import struct +import warnings +import zlib + +from .rencode import dumps, loads + +RPC_RESPONSE = 1 +RPC_ERROR = 2 +RPC_EVENT = 3 + +MESSAGE_HEADER_SIZE = 5 +READ_SIZE = 10 + +logger = logging.getLogger(__name__) + + +class DelugeClientException(Exception): + """Base exception for all deluge client exceptions""" + + +class ConnectionLostException(DelugeClientException): + pass + + +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, 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: + # 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 + + def disconnect(self): + """ + Disconnect from deluge + """ + if self.connected: + self._socket.close() + 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 + if method == 'daemon.login': + debug_args = list(args) + if len(debug_args) >= 2: + debug_args[1] = '