14 changed files with 768 additions and 460 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