Browse Source

Updated rtorrent-python library

- Implemented "requests" transport
 - Support for digest authentication (via the "requests" transport)

#4854
pull/5954/head
Dean Gardiner 9 years ago
parent
commit
5a9e67c93e
  1. 123
      libs/rtorrent/__init__.py
  2. 158
      libs/rtorrent/connection.py
  3. 0
      libs/rtorrent/lib/xmlrpc/clients/__init__.py
  4. 0
      libs/rtorrent/lib/xmlrpc/clients/http.py
  5. 152
      libs/rtorrent/lib/xmlrpc/clients/scgi.py
  6. 0
      libs/rtorrent/lib/xmlrpc/transports/__init__.py
  7. 0
      libs/rtorrent/lib/xmlrpc/transports/basic_auth.py
  8. 91
      libs/rtorrent/lib/xmlrpc/transports/requests_.py
  9. 72
      libs/rtorrent/lib/xmlrpc/transports/scgi.py
  10. 6
      libs/rtorrent/rpc/__init__.py

123
libs/rtorrent/__init__.py

@ -22,13 +22,11 @@ import os.path
import time import time
import xmlrpclib import xmlrpclib
from rtorrent.connection import Connection
from rtorrent.common import find_torrent, join_uri, \ from rtorrent.common import find_torrent, join_uri, \
update_uri, is_valid_port, convert_version_tuple_to_str update_uri, is_valid_port, convert_version_tuple_to_str
from rtorrent.lib.torrentparser import TorrentParser 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.rpc import Method
from rtorrent.lib.xmlrpc.basic_auth import BasicAuthTransport
from rtorrent.torrent import Torrent from rtorrent.torrent import Torrent
from rtorrent.group import Group from rtorrent.group import Group
import rtorrent.rpc # @UnresolvedImport import rtorrent.rpc # @UnresolvedImport
@ -38,119 +36,28 @@ __author__ = "Chris Lucas"
__contact__ = "chris@chrisjlucas.com" __contact__ = "chris@chrisjlucas.com"
__license__ = "MIT" __license__ = "MIT"
MIN_RTORRENT_VERSION = (0, 8, 1)
MIN_RTORRENT_VERSION_STR = convert_version_tuple_to_str(MIN_RTORRENT_VERSION)
class RTorrent: class RTorrent:
""" Create a new rTorrent connection """ """ Create a new rTorrent connection """
rpc_prefix = None rpc_prefix = None
def __init__(self, uri, username=None, password=None, def __init__(self, uri, auth=None, verify_server=False, verify_ssl=True, sp=None, sp_kwargs=None):
verify=False, sp=None, sp_kwargs=None):
self.uri = self._transform_uri(uri) # : From X{__init__(self, url)}
self.username = username
self.password = password
self.scheme = urllib.splittype(self.uri)[0]
if sp:
self.sp = sp
elif self.scheme in ['http', 'https']:
self.sp = HTTPServerProxy
elif self.scheme == 'scgi':
self.sp = SCGIServerProxy
else:
raise NotImplementedError()
self.sp_kwargs = sp_kwargs or {}
self.connection = Connection(uri, auth, verify_ssl, sp, sp_kwargs)
self.torrents = [] # : List of L{Torrent} instances self.torrents = [] # : List of L{Torrent} instances
self._rpc_methods = [] # : List of rTorrent RPC methods
self._torrent_cache = []
self._client_version_tuple = ()
if verify is True:
self._verify_conn()
def _transform_uri(self, uri): self._torrent_cache = []
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 # Verify connection is valid
uri = join_uri(uri, 'plugins/httprpc/action.php', construct=False) if verify_server is True:
return update_uri(uri, scheme=transport) self.connection.verify()
return uri @property
def client(self):
return self.connection.client
def _get_conn(self): def _get_conn(self):
"""Get ServerProxy instance""" return self.client
if self.username and self.password:
if self.scheme == 'scgi':
raise NotImplementedError()
secure = self.scheme == 'https'
return self.sp(
self.uri,
transport=BasicAuthTransport(secure, self.username, self.password),
**self.sp_kwargs
)
return self.sp(self.uri, **self.sp_kwargs)
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."
# minimum rTorrent version check
assert self._meets_version_requirement() is True,\
"Error: Minimum rTorrent version required is {0}".format(
MIN_RTORRENT_VERSION_STR)
def test_connection(self):
try:
self._verify_conn()
except:
return False
return True
def _meets_version_requirement(self):
return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION
def _get_client_version_tuple(self):
conn = self._get_conn()
if not self._client_version_tuple:
if not hasattr(self, "client_version"):
setattr(self, "client_version",
conn.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 _update_rpc_methods(self):
self._rpc_methods = self._get_conn().system.listMethods()
return self._rpc_methods
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())
def get_torrents(self, view="main"): def get_torrents(self, view="main"):
"""Get list of all torrents in specified view """Get list of all torrents in specified view
@ -341,7 +248,7 @@ class RTorrent:
assert view is not None, "view parameter required on non-persistent groups" assert view is not None, "view parameter required on non-persistent groups"
p.group.insert('', name, view) p.group.insert('', name, view)
self._update_rpc_methods() self.connection._update_rpc_methods()
def get_group(self, name): def get_group(self, name):
assert name is not None, "group name required" assert name is not None, "group name required"
@ -424,8 +331,8 @@ def _build_class_methods(class_obj):
def __compare_rpc_methods(rt_new, rt_old): def __compare_rpc_methods(rt_new, rt_old):
from pprint import pprint from pprint import pprint
rt_new_methods = set(rt_new._get_rpc_methods()) rt_new_methods = set(rt_new.connection._get_rpc_methods())
rt_old_methods = set(rt_old._get_rpc_methods()) rt_old_methods = set(rt_old.connection._get_rpc_methods())
print("New Methods:") print("New Methods:")
pprint(rt_new_methods - rt_old_methods) pprint(rt_new_methods - rt_old_methods)
print("Methods not in new rTorrent:") print("Methods not in new rTorrent:")
@ -440,7 +347,7 @@ def __check_supported_methods(rt):
rtorrent.torrent.methods + rtorrent.torrent.methods +
rtorrent.tracker.methods + rtorrent.tracker.methods +
rtorrent.peer.methods]) rtorrent.peer.methods])
all_methods = set(rt._get_rpc_methods()) all_methods = set(rt.connection._get_rpc_methods())
print("Methods NOT in supported methods") print("Methods NOT in supported methods")
pprint(all_methods - supported_methods) pprint(all_methods - supported_methods)

158
libs/rtorrent/connection.py

@ -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
libs/rtorrent/lib/xmlrpc/clients/__init__.py

0
libs/rtorrent/lib/xmlrpc/http.py → libs/rtorrent/lib/xmlrpc/clients/http.py

152
libs/rtorrent/lib/xmlrpc/clients/scgi.py

@ -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
libs/rtorrent/lib/xmlrpc/transports/__init__.py

0
libs/rtorrent/lib/xmlrpc/basic_auth.py → libs/rtorrent/lib/xmlrpc/transports/basic_auth.py

91
libs/rtorrent/lib/xmlrpc/transports/requests_.py

@ -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()

72
libs/rtorrent/lib/xmlrpc/scgi.py → libs/rtorrent/lib/xmlrpc/transports/scgi.py

@ -28,12 +28,12 @@
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or # the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version. # (at your option) any later version.
# #
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
@ -81,12 +81,13 @@
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE. # OF THIS SOFTWARE.
import errno
import httplib import httplib
import re import re
import socket import socket
import urllib import urllib
import xmlrpclib import xmlrpclib
import errno
class SCGITransport(xmlrpclib.Transport): class SCGITransport(xmlrpclib.Transport):
@ -152,68 +153,3 @@ class SCGITransport(xmlrpclib.Transport):
p.close() p.close()
return u.close() return u.close()
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,))

6
libs/rtorrent/rpc/__init__.py

@ -45,7 +45,7 @@ def get_varname(rpc_call):
def _handle_unavailable_rpc_method(method, rt_obj): def _handle_unavailable_rpc_method(method, rt_obj):
msg = "Method isn't available." msg = "Method isn't available."
if rt_obj._get_client_version_tuple() < method.min_version: if rt_obj.connection._get_client_version_tuple() < method.min_version:
msg = "This method is only available in " \ msg = "This method is only available in " \
"RTorrent version v{0} or later".format( "RTorrent version v{0} or later".format(
convert_version_tuple_to_str(method.min_version)) convert_version_tuple_to_str(method.min_version))
@ -108,8 +108,8 @@ class Method:
return(False) return(False)
def is_available(self, rt_obj): def is_available(self, rt_obj):
if rt_obj._get_client_version_tuple() < self.min_version or \ if rt_obj.connection._get_client_version_tuple() < self.min_version or \
self.rpc_call not in rt_obj._get_rpc_methods(): self.rpc_call not in rt_obj.connection._get_rpc_methods():
return(False) return(False)
else: else:
return(True) return(True)

Loading…
Cancel
Save