From 3166f29d345141062a9ec971f4349570e1b134ce Mon Sep 17 00:00:00 2001 From: Alexandre Beloin Date: Thu, 29 Jan 2015 23:53:29 -0500 Subject: [PATCH] Improved rTorrent support. Now use requests library. Added SSL support with Basic and Digest support. --- .../interfaces/default/config_search.tmpl | 19 +- gui/slick/js/configSearch.js | 9 + lib/rtorrent/__init__.py | 68 +++++-- lib/rtorrent/lib/xmlrpc/requests_transport.py | 188 ++++++++++++++++++ sickbeard/__init__.py | 5 +- sickbeard/clients/rtorrent.py | 9 +- sickbeard/webserve.py | 3 +- 7 files changed, 279 insertions(+), 22 deletions(-) create mode 100644 lib/rtorrent/lib/xmlrpc/requests_transport.py diff --git a/gui/slick/interfaces/default/config_search.tmpl b/gui/slick/interfaces/default/config_search.tmpl index c41bb331..a930087b 100755 --- a/gui/slick/interfaces/default/config_search.tmpl +++ b/gui/slick/interfaces/default/config_search.tmpl @@ -466,12 +466,29 @@ +
+ +
+
diff --git a/gui/slick/js/configSearch.js b/gui/slick/js/configSearch.js index f75ea622..4e9aef84 100644 --- a/gui/slick/js/configSearch.js +++ b/gui/slick/js/configSearch.js @@ -69,6 +69,9 @@ $(document).ready(function(){ $(host_desc_rtorrent).hide(); $(host_desc_torrent).show(); $(torrent_verify_cert_option).hide(); + $(torrent_verify_deluge).hide(); + $(torrent_verify_rtorrent).hide(); + $(torrent_auth_type).hide(); $(torrent_path_option).show(); $(torrent_path_option).find('.fileBrowser').show(); $(torrent_seed_time_option).hide(); @@ -96,6 +99,8 @@ $(document).ready(function(){ } else if ('deluge' == selectedProvider){ client = 'Deluge'; $(torrent_verify_cert_option).show(); + $(torrent_verify_deluge).show(); + $(torrent_verify_rtorrent).hide(); $(label_warning_deluge).show(); $(label_anime_warning_deluge).show(); $('#host_desc_torrent').text('URL to your Deluge client (e.g. http://localhost:8112)'); @@ -113,6 +118,10 @@ $(document).ready(function(){ client = 'rTorrent'; $(torrent_paused_option).hide(); $('#host_desc_torrent').text('URL to your rTorrent client (e.g. scgi://localhost:5000
or https://localhost/rutorrent/plugins/httprpc/action.php)'); + $(torrent_verify_cert_option).show(); + $(torrent_verify_deluge).hide(); + $(torrent_verify_rtorrent).show(); + $(torrent_auth_type).show(); //$('#directory_title').text(client + directory); } $('#host_title').text(client + host); diff --git a/lib/rtorrent/__init__.py b/lib/rtorrent/__init__.py index 290ef115..c24f608f 100644 --- a/lib/rtorrent/__init__.py +++ b/lib/rtorrent/__init__.py @@ -22,15 +22,16 @@ import os.path import time import xmlrpclib -from rtorrent.common import find_torrent, \ - is_valid_port, convert_version_tuple_to_str -from rtorrent.lib.torrentparser import TorrentParser -from rtorrent.lib.xmlrpc.http import HTTPServerProxy -from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy -from rtorrent.rpc import Method -from rtorrent.lib.xmlrpc.basic_auth import BasicAuthTransport -from rtorrent.torrent import Torrent -from rtorrent.group import Group +from rtorrent.common import (find_torrent, # @UnresolvedImport + is_valid_port, # @UnresolvedImport + convert_version_tuple_to_str) # @UnresolvedImport +from rtorrent.lib.torrentparser import TorrentParser # @UnresolvedImport +from rtorrent.lib.xmlrpc.http import HTTPServerProxy # @UnresolvedImport +from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy # @UnresolvedImport +from rtorrent.rpc import Method # @UnresolvedImport +from rtorrent.lib.xmlrpc.requests_transport import RequestsTransport # @UnresolvedImport @IgnorePep8 +from rtorrent.torrent import Torrent # @UnresolvedImport +from rtorrent.group import Group # @UnresolvedImport import rtorrent.rpc # @UnresolvedImport __version__ = "0.2.9" @@ -43,11 +44,12 @@ MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) class RTorrent: + """ Create a new rTorrent connection """ rpc_prefix = None def __init__(self, uri, username=None, password=None, - verify=False, sp=None, sp_kwargs=None): + verify=False, sp=None, sp_kwargs=None, tp_kwargs=None): self.uri = uri # : From X{__init__(self, url)} self.username = username @@ -59,6 +61,10 @@ class RTorrent: self.sp = sp elif self.schema in ['http', 'https']: self.sp = HTTPServerProxy + if self.schema == 'https': + self.isHttps = True + else: + self.isHttps = False elif self.schema == 'scgi': self.sp = SCGIServerProxy else: @@ -66,6 +72,8 @@ class RTorrent: self.sp_kwargs = sp_kwargs or {} + self.tp_kwargs = tp_kwargs or {} + self.torrents = [] # : List of L{Torrent} instances self._rpc_methods = [] # : List of rTorrent RPC methods self._torrent_cache = [] @@ -80,9 +88,30 @@ class RTorrent: if self.schema == 'scgi': raise NotImplementedError() + if 'authtype' not in self.tp_kwargs: + authtype = None + else: + authtype = self.tp_kwargs['authtype'] + + if 'check_ssl_cert' not in self.tp_kwargs: + check_ssl_cert = True + else: + check_ssl_cert = self.tp_kwargs['check_ssl_cert'] + + if 'proxies' not in self.tp_kwargs: + proxies = None + else: + proxies = self.tp_kwargs['proxies'] + return self.sp( self.uri, - transport=BasicAuthTransport(self.username, self.password), + transport=RequestsTransport( + use_https=self.isHttps, + authtype=authtype, + username=self.username, + password=self.password, + check_ssl_cert=check_ssl_cert, + proxies=proxies), **self.sp_kwargs ) @@ -90,8 +119,10 @@ class RTorrent: def _verify_conn(self): # check for rpc methods that should be available - assert "system.client_version" in self._get_rpc_methods(), "Required RPC method not available." - assert "system.library_version" in self._get_rpc_methods(), "Required RPC method not available." + assert "system.client_version" in self._get_rpc_methods( + ), "Required RPC method not available." + assert "system.library_version" in self._get_rpc_methods( + ), "Required RPC method not available." # minimum rTorrent version check assert self._meets_version_requirement() is True,\ @@ -152,7 +183,8 @@ class RTorrent: for result in results: results_dict = {} # build results_dict - for m, r in zip(retriever_methods, result[1:]): # result[0] is the info_hash + # result[0] is the info_hash + for m, r in zip(retriever_methods, result[1:]): results_dict[m.varname] = rtorrent.rpc.process_result(m, r) self.torrents.append( @@ -199,7 +231,7 @@ class RTorrent: return(func_name) - def load_magnet(self, magneturl, info_hash, start=False, verbose=False, verify_load=True): + def load_magnet(self, magneturl, info_hash, start=False, verbose=False, verify_load=True): # @IgnorePep8 p = self._get_conn() @@ -231,13 +263,13 @@ class RTorrent: while i < MAX_RETRIES: for torrent in self.get_torrents(): if torrent.info_hash == info_hash: - if str(info_hash) not in str(torrent.name) : + if str(info_hash) not in str(torrent.name): time.sleep(1) i += 1 return(torrent) - def load_torrent(self, torrent, start=False, verbose=False, verify_load=True): + def load_torrent(self, torrent, start=False, verbose=False, verify_load=True): # @IgnorePep8 """ Loads torrent into rTorrent (with various enhancements) @@ -354,7 +386,7 @@ class RTorrent: if persistent is True: p.group.insert_persistent_view('', name) else: - assert view is not None, "view parameter required on non-persistent groups" + assert view is not None, "view parameter required on non-persistent groups" # @IgnorePep8 p.group.insert('', name, view) self._update_rpc_methods() diff --git a/lib/rtorrent/lib/xmlrpc/requests_transport.py b/lib/rtorrent/lib/xmlrpc/requests_transport.py new file mode 100644 index 00000000..d5e28743 --- /dev/null +++ b/lib/rtorrent/lib/xmlrpc/requests_transport.py @@ -0,0 +1,188 @@ +# Copyright (c) 2013-2015 Alexandre Beloin, +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""A transport for Python2/3 xmlrpc library using requests + +Support: +-SSL with Basic and Digest authentication +-Proxies +""" + +try: + import xmlrpc.client as xmlrpc_client +except ImportError: + import xmlrpclib as xmlrpc_client + +import traceback + +import requests +from requests.exceptions import RequestException +from requests.auth import HTTPBasicAuth +from requests.auth import HTTPDigestAuth +from requests.packages.urllib3 import disable_warnings # @UnresolvedImport + + +class RequestsTransport(xmlrpc_client.Transport): + + """Transport class for xmlrpc using requests""" + + def __init__(self, use_https=True, authtype=None, username=None, + password=None, check_ssl_cert=True, proxies=None): + """Inits RequestsTransport. + + Args: + use_https: If true, https else http + authtype: None, basic or digest + username: Username + password: Password + check_ssl_cert: Check SSL certificate + proxies: A dict of proxies( + Ex: {"http": "http://10.10.1.10:3128", + "https": "http://10.10.1.10:1080",}) + + Raises: + ValueError: Invalid info + """ + # Python 2 can't use super on old style class. + if issubclass(xmlrpc_client.Transport, object): + super(RequestsTransport, self).__init__() + else: + xmlrpc_client.Transport.__init__(self) + + self.user_agent = "Python Requests/" + requests.__version__ + + self._use_https = use_https + self._check_ssl_cert = check_ssl_cert + + if authtype == "basic" or authtype == "digest": + self._authtype = authtype + else: + raise ValueError( + "Supported authentication are: basic and digest") + if authtype and (not username or not password): + raise ValueError( + "Username and password required when using authentication") + + self._username = username + self._password = password + if proxies is None: + self._proxies = {} + else: + self._proxies = proxies + + def request(self, host, handler, request_body, verbose=0): + """Replace the xmlrpc request function. + + Process xmlrpc request via requests library. + + Args: + host: Target host + handler: Target PRC handler. + request_body: XML-RPC request body. + verbose: Debugging flag. + + Returns: + Parsed response. + + Raises: + RequestException: Error in requests + """ + if verbose: + self._debug() + + if not self._check_ssl_cert: + disable_warnings() + + headers = {'User-Agent': self.user_agent, 'Content-Type': 'text/xml', } + + # Need to be done because the schema(http or https) is lost in + # xmlrpc.Transport's init. + if self._use_https: + url = "https://{host}/{handler}".format(host=host, handler=handler) + else: + url = "http://{host}/{handler}".format(host=host, handler=handler) + + # TODO Construct kwargs query instead + try: + if self._authtype == "basic": + response = requests.post( + url, + data=request_body, + headers=headers, + verify=self._check_ssl_cert, + auth=HTTPBasicAuth( + self._username, self._password), + proxies=self._proxies) + elif self._authtype == "digest": + response = requests.post( + url, + data=request_body, + headers=headers, + verify=self._check_ssl_cert, + auth=HTTPDigestAuth( + self._username, self._password), + proxies=self._proxies) + else: + response = requests.post( + url, + data=request_body, + headers=headers, + verify=self._check_ssl_cert, + proxies=self._proxies) + + response.raise_for_status() + except RequestException as error: + raise xmlrpc_client.ProtocolError(url, + error.message, + traceback.format_exc(), + response.headers) + + return self.parse_response(response) + + def parse_response(self, response): + """Replace the xmlrpc parse_response function. + + Parse response. + + Args: + response: Requests return data + + Returns: + Response tuple and target method. + """ + p, u = self.getparser() + p.feed(response.text) + p.close() + return u.close() + + def _debug(self): + """Debug requests module. + + Enable verbose logging from requests + """ + # TODO Ugly + import logging + try: + import http.client as http_client + except ImportError: + import httplib as http_client + + http_client.HTTPConnection.debuglevel = 1 + + logging.basicConfig() + logging.getLogger().setLevel(logging.DEBUG) + requests_log = logging.getLogger("requests.packages.urllib3") + requests_log.setLevel(logging.DEBUG) + requests_log.propagate = True diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 22a240f1..23d13f90 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -288,6 +288,7 @@ TORRENT_LABEL = '' TORRENT_LABEL_ANIME = '' TORRENT_VERIFY_CERT = False TORRENT_RPCURL = 'transmission' +TORRENT_AUTH_TYPE = 'none' USE_KODI = False KODI_ALWAYS_ON = True @@ -503,7 +504,7 @@ def initialize(consoleLogging=True): HANDLE_REVERSE_PROXY, USE_NZBS, USE_TORRENTS, NZB_METHOD, NZB_DIR, DOWNLOAD_PROPERS, RANDOMIZE_PROVIDERS, CHECK_PROPERS_INTERVAL, ALLOW_HIGH_PRIORITY, TORRENT_METHOD, \ SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_CATEGORY_ANIME, SAB_HOST, \ NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_CATEGORY_ANIME, NZBGET_PRIORITY, NZBGET_HOST, NZBGET_USE_HTTPS, backlogSearchScheduler, \ - TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_LABEL_ANIME, TORRENT_VERIFY_CERT, TORRENT_RPCURL, \ + TORRENT_USERNAME, TORRENT_PASSWORD, TORRENT_HOST, TORRENT_PATH, TORRENT_SEED_TIME, TORRENT_PAUSED, TORRENT_HIGH_BANDWIDTH, TORRENT_LABEL, TORRENT_LABEL_ANIME, TORRENT_VERIFY_CERT, TORRENT_RPCURL, TORRENT_AUTH_TYPE, \ USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, \ KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, BACKLOG_FREQUENCY, \ USE_TRAKT, TRAKT_USERNAME, TRAKT_PASSWORD, TRAKT_REMOVE_WATCHLIST, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_DISABLE_SSL_VERIFY, TRAKT_TIMEOUT, \ @@ -816,6 +817,7 @@ def initialize(consoleLogging=True): TORRENT_LABEL_ANIME = check_setting_str(CFG, 'TORRENT', 'torrent_label_anime', '') TORRENT_VERIFY_CERT = bool(check_setting_int(CFG, 'TORRENT', 'torrent_verify_cert', 0)) TORRENT_RPCURL = check_setting_str(CFG, 'TORRENT', 'torrent_rpcurl', 'transmission') + TORRENT_AUTH_TYPE = check_setting_str(CFG, 'TORRENT', 'torrent_auth_type', '') USE_KODI = bool(check_setting_int(CFG, 'KODI', 'use_kodi', 0)) KODI_ALWAYS_ON = bool(check_setting_int(CFG, 'KODI', 'kodi_always_on', 1)) @@ -1681,6 +1683,7 @@ def save_config(): new_config['TORRENT']['torrent_label_anime'] = TORRENT_LABEL_ANIME new_config['TORRENT']['torrent_verify_cert'] = int(TORRENT_VERIFY_CERT) new_config['TORRENT']['torrent_rpcurl'] = TORRENT_RPCURL + new_config['TORRENT']['torrent_auth_type'] = TORRENT_AUTH_TYPE new_config['KODI'] = {} new_config['KODI']['use_kodi'] = int(USE_KODI) diff --git a/sickbeard/clients/rtorrent.py b/sickbeard/clients/rtorrent.py index 82f76046..3a8a865d 100644 --- a/sickbeard/clients/rtorrent.py +++ b/sickbeard/clients/rtorrent.py @@ -37,8 +37,15 @@ class rTorrentAPI(GenericClient): if not self.host: return + tp_kwargs = {} + if sickbeard.TORRENT_AUTH_TYPE is not 'none': + tp_kwargs['authtype'] = sickbeard.TORRENT_AUTH_TYPE + + if not sickbeard.TORRENT_VERIFY_CERT: + tp_kwargs['check_ssl_cert'] = False + if self.username and self.password: - self.auth = RTorrent(self.host, self.username, self.password) + self.auth = RTorrent(self.host, self.username, self.password, True, tp_kwargs=tp_kwargs) else: self.auth = RTorrent(self.host, None, None, True) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c2c1e8e0..b78ff851 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -3656,7 +3656,7 @@ class ConfigSearch(Config): torrent_dir=None, torrent_username=None, torrent_password=None, torrent_host=None, torrent_label=None, torrent_label_anime=None, torrent_path=None, torrent_verify_cert=None, torrent_seed_time=None, torrent_paused=None, torrent_high_bandwidth=None, - torrent_rpcurl=None, ignore_words=None, require_words=None): + torrent_rpcurl=None, torrent_auth_type = None, ignore_words=None, require_words=None): results = [] @@ -3717,6 +3717,7 @@ class ConfigSearch(Config): sickbeard.TORRENT_HIGH_BANDWIDTH = config.checkbox_to_value(torrent_high_bandwidth) sickbeard.TORRENT_HOST = config.clean_url(torrent_host) sickbeard.TORRENT_RPCURL = torrent_rpcurl + sickbeard.TORRENT_AUTH_TYPE = torrent_auth_type sickbeard.save_config()