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] = '