Browse Source
- Implemented "requests" transport - Support for digest authentication (via the "requests" transport) #4854pull/5954/head
13 changed files with 692 additions and 448 deletions
@ -0,0 +1,158 @@ |
|||||
|
import logging |
||||
|
import urllib |
||||
|
|
||||
|
from rtorrent.common import convert_version_tuple_to_str, join_uri, update_uri |
||||
|
from rtorrent.lib.xmlrpc.clients.http import HTTPServerProxy |
||||
|
from rtorrent.lib.xmlrpc.clients.scgi import SCGIServerProxy |
||||
|
from rtorrent.lib.xmlrpc.transports.basic_auth import BasicAuthTransport |
||||
|
|
||||
|
# Try import requests transport (optional) |
||||
|
try: |
||||
|
from rtorrent.lib.xmlrpc.transports.requests_ import RequestsTransport |
||||
|
except ImportError: |
||||
|
RequestsTransport = None |
||||
|
|
||||
|
MIN_RTORRENT_VERSION = (0, 8, 1) |
||||
|
MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION) |
||||
|
|
||||
|
log = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class Connection(object): |
||||
|
def __init__(self, uri, auth=None, verify_ssl=True, sp=None, sp_kwargs=None): |
||||
|
self.auth = auth |
||||
|
self.verify_ssl = verify_ssl |
||||
|
|
||||
|
# Transform + Parse URI |
||||
|
self.uri = self._transform_uri(uri) |
||||
|
self.scheme = urllib.splittype(self.uri)[0] |
||||
|
|
||||
|
# Construct RPC Client |
||||
|
self.sp = self._get_sp(self.scheme, sp) |
||||
|
self.sp_kwargs = sp_kwargs or {} |
||||
|
|
||||
|
self._client = None |
||||
|
self._client_version_tuple = () |
||||
|
self._rpc_methods = [] |
||||
|
|
||||
|
@property |
||||
|
def client(self): |
||||
|
if self._client is None: |
||||
|
# Construct new client |
||||
|
self._client = self.connect() |
||||
|
|
||||
|
# Return client |
||||
|
return self._client |
||||
|
|
||||
|
def connect(self): |
||||
|
log.debug('Connecting to server: %r', self.uri) |
||||
|
|
||||
|
if self.auth: |
||||
|
# Construct server proxy with authentication transport |
||||
|
return self.sp(self.uri, transport=self._construct_transport(), **self.sp_kwargs) |
||||
|
|
||||
|
# Construct plain server proxy |
||||
|
return self.sp(self.uri, **self.sp_kwargs) |
||||
|
|
||||
|
def test(self): |
||||
|
try: |
||||
|
self.verify() |
||||
|
except: |
||||
|
return False |
||||
|
|
||||
|
return True |
||||
|
|
||||
|
def verify(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." |
||||
|
|
||||
|
# minimum rTorrent version check |
||||
|
assert self._meets_version_requirement() is True,\ |
||||
|
"Error: Minimum rTorrent version required is {0}".format(MIN_RTORRENT_VERSION_STR) |
||||
|
|
||||
|
# |
||||
|
# Private methods |
||||
|
# |
||||
|
|
||||
|
def _construct_transport(self): |
||||
|
# Ensure "auth" parameter is valid |
||||
|
if type(self.auth) is not tuple or len(self.auth) != 3: |
||||
|
raise ValueError('Invalid "auth" parameter format') |
||||
|
|
||||
|
# Construct transport with authentication details |
||||
|
method, _, _ = self.auth |
||||
|
secure = self.scheme == 'https' |
||||
|
|
||||
|
log.debug('Constructing transport for scheme: %r, authentication method: %r', self.scheme, method) |
||||
|
|
||||
|
# Use requests transport (if available) |
||||
|
if RequestsTransport and method in ['basic', 'digest']: |
||||
|
return RequestsTransport( |
||||
|
secure, self.auth, |
||||
|
verify_ssl=self.verify_ssl |
||||
|
) |
||||
|
|
||||
|
# Use basic authentication transport |
||||
|
if method == 'basic': |
||||
|
return BasicAuthTransport(secure, self.auth) |
||||
|
|
||||
|
# Unsupported authentication method |
||||
|
if method == 'digest': |
||||
|
raise Exception('Digest authentication requires the "requests" library') |
||||
|
|
||||
|
raise NotImplementedError('Unknown authentication method: %r' % method) |
||||
|
|
||||
|
def _get_client_version_tuple(self): |
||||
|
if not self._client_version_tuple: |
||||
|
if not hasattr(self, "client_version"): |
||||
|
setattr(self, "client_version", self.client.system.client_version()) |
||||
|
|
||||
|
rtver = getattr(self, "client_version") |
||||
|
self._client_version_tuple = tuple([int(i) for i in rtver.split(".")]) |
||||
|
|
||||
|
return self._client_version_tuple |
||||
|
|
||||
|
def _get_rpc_methods(self): |
||||
|
""" Get list of raw RPC commands |
||||
|
|
||||
|
@return: raw RPC commands |
||||
|
@rtype: list |
||||
|
""" |
||||
|
|
||||
|
return(self._rpc_methods or self._update_rpc_methods()) |
||||
|
|
||||
|
@staticmethod |
||||
|
def _get_sp(scheme, sp): |
||||
|
if sp: |
||||
|
return sp |
||||
|
|
||||
|
if scheme in ['http', 'https']: |
||||
|
return HTTPServerProxy |
||||
|
|
||||
|
if scheme == 'scgi': |
||||
|
return SCGIServerProxy |
||||
|
|
||||
|
raise NotImplementedError() |
||||
|
|
||||
|
def _meets_version_requirement(self): |
||||
|
return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION |
||||
|
|
||||
|
@staticmethod |
||||
|
def _transform_uri(uri): |
||||
|
scheme = urllib.splittype(uri)[0] |
||||
|
|
||||
|
if scheme == 'httprpc' or scheme.startswith('httprpc+'): |
||||
|
# Try find HTTPRPC transport (token after '+' in 'httprpc+https'), otherwise assume HTTP |
||||
|
transport = scheme[scheme.index('+') + 1:] if '+' in scheme else 'http' |
||||
|
|
||||
|
# Transform URI with new path and scheme |
||||
|
uri = join_uri(uri, 'plugins/httprpc/action.php', construct=False) |
||||
|
return update_uri(uri, scheme=transport) |
||||
|
|
||||
|
return uri |
||||
|
|
||||
|
def _update_rpc_methods(self): |
||||
|
self._rpc_methods = self.client.system.listMethods() |
||||
|
|
||||
|
return self._rpc_methods |
@ -0,0 +1,152 @@ |
|||||
|
#!/usr/bin/python |
||||
|
|
||||
|
# rtorrent_xmlrpc |
||||
|
# (c) 2011 Roger Que <alerante@bellsouth.net> |
||||
|
# |
||||
|
# Modified portions: |
||||
|
# (c) 2013 Dean Gardiner <gardiner91@gmail.com> |
||||
|
# |
||||
|
# Python module for interacting with rtorrent's XML-RPC interface |
||||
|
# directly over SCGI, instead of through an HTTP server intermediary. |
||||
|
# Inspired by Glenn Washburn's xmlrpc2scgi.py [1], but subclasses the |
||||
|
# built-in xmlrpclib classes so that it is compatible with features |
||||
|
# such as MultiCall objects. |
||||
|
# |
||||
|
# [1] <http://libtorrent.rakshasa.no/wiki/UtilsXmlrpc2scgi> |
||||
|
# |
||||
|
# Usage: server = SCGIServerProxy('scgi://localhost:7000/') |
||||
|
# server = SCGIServerProxy('scgi:///path/to/scgi.sock') |
||||
|
# print server.system.listMethods() |
||||
|
# mc = xmlrpclib.MultiCall(server) |
||||
|
# mc.get_up_rate() |
||||
|
# mc.get_down_rate() |
||||
|
# print mc() |
||||
|
# |
||||
|
# |
||||
|
# |
||||
|
# 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 2 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, write to the Free Software |
||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
||||
|
# |
||||
|
# In addition, as a special exception, the copyright holders give |
||||
|
# permission to link the code of portions of this program with the |
||||
|
# OpenSSL library under certain conditions as described in each |
||||
|
# individual source file, and distribute linked combinations |
||||
|
# including the two. |
||||
|
# |
||||
|
# You must obey the GNU General Public License in all respects for |
||||
|
# all of the code used other than OpenSSL. If you modify file(s) |
||||
|
# with this exception, you may extend this exception to your version |
||||
|
# of the file(s), but you are not obligated to do so. If you do not |
||||
|
# wish to do so, delete this exception statement from your version. |
||||
|
# If you delete this exception statement from all source files in the |
||||
|
# program, then also delete it here. |
||||
|
# |
||||
|
# |
||||
|
# |
||||
|
# Portions based on Python's xmlrpclib: |
||||
|
# |
||||
|
# Copyright (c) 1999-2002 by Secret Labs AB |
||||
|
# Copyright (c) 1999-2002 by Fredrik Lundh |
||||
|
# |
||||
|
# By obtaining, using, and/or copying this software and/or its |
||||
|
# associated documentation, you agree that you have read, understood, |
||||
|
# and will comply with the following terms and conditions: |
||||
|
# |
||||
|
# Permission to use, copy, modify, and distribute this software and |
||||
|
# its associated documentation for any purpose and without fee is |
||||
|
# hereby granted, provided that the above copyright notice appears in |
||||
|
# all copies, and that both that copyright notice and this permission |
||||
|
# notice appear in supporting documentation, and that the name of |
||||
|
# Secret Labs AB or the author not be used in advertising or publicity |
||||
|
# pertaining to distribution of the software without specific, written |
||||
|
# prior permission. |
||||
|
# |
||||
|
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD |
||||
|
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- |
||||
|
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR |
||||
|
# BE LIABLE FOR ANY SPECIAL, 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. |
||||
|
|
||||
|
import urllib |
||||
|
import xmlrpclib |
||||
|
|
||||
|
from rtorrent.lib.xmlrpc.transports.scgi import SCGITransport |
||||
|
|
||||
|
|
||||
|
class SCGIServerProxy(xmlrpclib.ServerProxy): |
||||
|
def __init__(self, uri, transport=None, encoding=None, verbose=False, |
||||
|
allow_none=False, use_datetime=False): |
||||
|
type, uri = urllib.splittype(uri) |
||||
|
if type not in ('scgi'): |
||||
|
raise IOError('unsupported XML-RPC protocol') |
||||
|
self.__host, self.__handler = urllib.splithost(uri) |
||||
|
if not self.__handler: |
||||
|
self.__handler = '/' |
||||
|
|
||||
|
if transport is None: |
||||
|
transport = SCGITransport(use_datetime=use_datetime) |
||||
|
self.__transport = transport |
||||
|
|
||||
|
self.__encoding = encoding |
||||
|
self.__verbose = verbose |
||||
|
self.__allow_none = allow_none |
||||
|
|
||||
|
def __close(self): |
||||
|
self.__transport.close() |
||||
|
|
||||
|
def __request(self, methodname, params): |
||||
|
# call a method on the remote server |
||||
|
|
||||
|
request = xmlrpclib.dumps(params, methodname, encoding=self.__encoding, |
||||
|
allow_none=self.__allow_none) |
||||
|
|
||||
|
response = self.__transport.request( |
||||
|
self.__host, |
||||
|
self.__handler, |
||||
|
request, |
||||
|
verbose=self.__verbose |
||||
|
) |
||||
|
|
||||
|
if len(response) == 1: |
||||
|
response = response[0] |
||||
|
|
||||
|
return response |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return ( |
||||
|
"<SCGIServerProxy for %s%s>" % |
||||
|
(self.__host, self.__handler) |
||||
|
) |
||||
|
|
||||
|
__str__ = __repr__ |
||||
|
|
||||
|
def __getattr__(self, name): |
||||
|
# magic method dispatcher |
||||
|
return xmlrpclib._Method(self.__request, name) |
||||
|
|
||||
|
# note: to call a remote object with an non-standard name, use |
||||
|
# result getattr(server, "strange-python-name")(args) |
||||
|
|
||||
|
def __call__(self, attr): |
||||
|
"""A workaround to get special attributes on the ServerProxy |
||||
|
without interfering with the magic __getattr__ |
||||
|
""" |
||||
|
if attr == "close": |
||||
|
return self.__close |
||||
|
elif attr == "transport": |
||||
|
return self.__transport |
||||
|
raise AttributeError("Attribute %r not found" % (attr,)) |
@ -0,0 +1,91 @@ |
|||||
|
import requests |
||||
|
import requests.auth |
||||
|
import xmlrpclib |
||||
|
|
||||
|
|
||||
|
class RequestsTransport(xmlrpclib.Transport): |
||||
|
def __init__(self, secure, auth=None, proxies=None, verify_ssl=True): |
||||
|
xmlrpclib.Transport.__init__(self) |
||||
|
|
||||
|
self.secure = secure |
||||
|
|
||||
|
# Construct session |
||||
|
self.session = requests.Session() |
||||
|
self.session.auth = self.parse_auth(auth) |
||||
|
self.session.proxies = proxies or {} |
||||
|
self.session.verify = verify_ssl |
||||
|
|
||||
|
@property |
||||
|
def scheme(self): |
||||
|
if self.secure: |
||||
|
return 'https' |
||||
|
|
||||
|
return 'http' |
||||
|
|
||||
|
def build_url(self, host, handler): |
||||
|
return '%s://%s' % (self.scheme, host + handler) |
||||
|
|
||||
|
def request(self, host, handler, request_body, verbose=0): |
||||
|
# Retry request once if cached connection has gone cold |
||||
|
for i in (0, 1): |
||||
|
try: |
||||
|
return self.single_request(host, handler, request_body, verbose) |
||||
|
except requests.ConnectionError: |
||||
|
if i: |
||||
|
raise |
||||
|
except requests.Timeout: |
||||
|
if i: |
||||
|
raise |
||||
|
|
||||
|
def single_request(self, host, handler, request_body, verbose=0): |
||||
|
url = self.build_url(host, handler) |
||||
|
|
||||
|
# Send request |
||||
|
response = self.session.post( |
||||
|
url, |
||||
|
data=request_body, |
||||
|
headers={ |
||||
|
'Content-Type': 'text/xml' |
||||
|
}, |
||||
|
stream=True |
||||
|
) |
||||
|
|
||||
|
if response.status_code == 200: |
||||
|
return self.parse_response(response) |
||||
|
|
||||
|
# Invalid response returned |
||||
|
raise xmlrpclib.ProtocolError( |
||||
|
host + handler, |
||||
|
response.status_code, response.reason, |
||||
|
response.headers |
||||
|
) |
||||
|
|
||||
|
def parse_auth(self, auth): |
||||
|
# Parse "auth" parameter |
||||
|
if type(auth) is not tuple or len(auth) != 3: |
||||
|
return None |
||||
|
|
||||
|
method, username, password = auth |
||||
|
|
||||
|
# Basic Authentication |
||||
|
if method == 'basic': |
||||
|
return requests.auth.HTTPBasicAuth(username, password) |
||||
|
|
||||
|
# Digest Authentication |
||||
|
if method == 'digest': |
||||
|
return requests.auth.HTTPDigestAuth(username, password) |
||||
|
|
||||
|
raise NotImplementedError('Unsupported authentication method: %r' % method) |
||||
|
|
||||
|
def parse_response(self, response): |
||||
|
p, u = self.getparser() |
||||
|
|
||||
|
# Write chunks to parser |
||||
|
for chunk in response.iter_content(1024): |
||||
|
p.feed(chunk) |
||||
|
|
||||
|
# Close parser |
||||
|
p.close() |
||||
|
|
||||
|
# Close unmarshaller |
||||
|
return u.close() |
Loading…
Reference in new issue