mirror of
https://github.com/codex-storage/deluge.git
synced 2025-01-11 12:04:10 +00:00
Test fixes and #1814 fix.
All test were adapted, and some more were added to comply with the new multiuser support in deluge. Regarding #1814, host entries in the Connection Manager UI are now migrated from the old format were automatic localhost logins were possible, which no longer is.
This commit is contained in:
parent
bb9a8509c8
commit
f41f6ad46a
@ -3,6 +3,8 @@
|
||||
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
|
||||
plugin, which is also shipped with Deluge, and it does a better job and
|
||||
now, it even supports multiple users perfectly.
|
||||
* Authentication/Permission exceptions are now sent to clients and recreated
|
||||
there to allow acting upon them.
|
||||
|
||||
==== Core ====
|
||||
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent
|
||||
@ -20,10 +22,16 @@
|
||||
* File modifications on the auth file are now detected and when they happen,
|
||||
the file is reloaded. Upon finding an old auth file with an old format, an
|
||||
upgrade to the new format is made, file saved, and reloaded.
|
||||
* Authentication no longer requires a username/password. If one or both of
|
||||
these is missing, an authentication error will be sent to the client
|
||||
which sould then ask the username/password to the user.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix uncaught exception when closing deluge in classic mode
|
||||
* Allow changing ownership of torrents
|
||||
* Host entries in the Connection Manager UI are now editable. They're
|
||||
now also migrated from the old format were automatic localhost logins were
|
||||
possible, which no longer is, this fixes #1814.
|
||||
|
||||
==== WebUI ====
|
||||
* Migrate to ExtJS 3.1
|
||||
|
@ -56,7 +56,7 @@ except ImportError:
|
||||
import deluge.component as component
|
||||
import deluge.configmanager
|
||||
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_ADMIN
|
||||
from deluge.error import DelugeError, NotAuthorizedError, AuthenticationRequired
|
||||
from deluge.error import DelugeError, NotAuthorizedError, __PassthroughError
|
||||
|
||||
RPC_RESPONSE = 1
|
||||
RPC_ERROR = 2
|
||||
@ -219,6 +219,9 @@ class DelugeRPCProtocol(Protocol):
|
||||
|
||||
log.info("Deluge client disconnected: %s", reason.value)
|
||||
|
||||
def valid_session(self):
|
||||
return self.transport.sessionno in self.factory.authorized_sessions
|
||||
|
||||
def dispatch(self, request_id, method, args, kwargs):
|
||||
"""
|
||||
This method is run when a RPC Request is made. It will run the local method
|
||||
@ -262,18 +265,23 @@ class DelugeRPCProtocol(Protocol):
|
||||
if ret:
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except AuthenticationRequired, err:
|
||||
self.sendData((RPC_EVENT_AUTH, request_id, err.message, args[0]))
|
||||
except Exception, e:
|
||||
sendError()
|
||||
log.exception(e)
|
||||
if isinstance(e, __PassthroughError):
|
||||
self.sendData(
|
||||
(RPC_EVENT_AUTH, request_id,
|
||||
e.__class__.__name__,
|
||||
e._args, e._kwargs, args[0])
|
||||
)
|
||||
else:
|
||||
sendError()
|
||||
log.exception(e)
|
||||
else:
|
||||
self.sendData((RPC_RESPONSE, request_id, (ret)))
|
||||
if not ret:
|
||||
self.transport.loseConnection()
|
||||
finally:
|
||||
return
|
||||
elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
elif method == "daemon.set_event_interest" and self.valid_session():
|
||||
log.debug("RPC dispatch daemon.set_event_interest")
|
||||
# This special case is to allow clients to set which events they are
|
||||
# interested in receiving.
|
||||
@ -289,22 +297,24 @@ class DelugeRPCProtocol(Protocol):
|
||||
finally:
|
||||
return
|
||||
|
||||
if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions:
|
||||
if method in self.factory.methods and self.valid_session():
|
||||
log.debug("RPC dispatch %s", method)
|
||||
try:
|
||||
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
|
||||
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
|
||||
if auth_level < method_auth_requirement:
|
||||
# This session is not allowed to call this method
|
||||
log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement))
|
||||
log.debug("Session %s is trying to call a method it is not "
|
||||
"authorized to call!", self.transport.sessionno)
|
||||
raise NotAuthorizedError(auth_level, method_auth_requirement)
|
||||
# Set the session_id in the factory so that methods can know
|
||||
# which session is calling it.
|
||||
self.factory.session_id = self.transport.sessionno
|
||||
ret = self.factory.methods[method](*args, **kwargs)
|
||||
except Exception, e:
|
||||
sendError()
|
||||
# Don't bother printing out DelugeErrors, because they are just for the client
|
||||
# Don't bother printing out DelugeErrors, because they are just
|
||||
# for the client
|
||||
if not isinstance(e, DelugeError):
|
||||
log.exception("Exception calling RPC request: %s", e)
|
||||
else:
|
||||
@ -545,8 +555,12 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(
|
||||
crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
)
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(
|
||||
crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
)
|
||||
# Make the files only readable by this user
|
||||
for f in ("daemon.pkey", "daemon.cert"):
|
||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||
|
@ -50,11 +50,24 @@ class InvalidTorrentError(DelugeError):
|
||||
class InvalidPathError(DelugeError):
|
||||
pass
|
||||
|
||||
class NotAuthorizedError(DelugeError):
|
||||
pass
|
||||
class __PassthroughError(DelugeError):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(__PassthroughError, cls).__new__(cls, *args, **kwargs)
|
||||
inst._args = args
|
||||
inst._kwargs = kwargs
|
||||
return inst
|
||||
|
||||
class NotAuthorizedError(__PassthroughError):
|
||||
def __init__(self, current_level, required_level):
|
||||
self.message = _(
|
||||
"Auth level too low: %(current_level)s < %(required_level)s" %
|
||||
dict(current_level=current_level, required_level=required_level)
|
||||
)
|
||||
self.current_level = current_level
|
||||
self.required_level = required_level
|
||||
|
||||
|
||||
class _UsernameBasedException(DelugeError):
|
||||
class __UsernameBasedPasstroughError(__PassthroughError):
|
||||
|
||||
def _get_message(self):
|
||||
return self._message
|
||||
@ -71,16 +84,16 @@ class _UsernameBasedException(DelugeError):
|
||||
del _get_username, _set_username
|
||||
|
||||
def __init__(self, message, username):
|
||||
super(_UsernameBasedException, self).__init__(message)
|
||||
super(__UsernameBasedPasstroughError, self).__init__(message)
|
||||
self.message = message
|
||||
self.username = username
|
||||
|
||||
|
||||
class BadLoginError(_UsernameBasedException):
|
||||
class BadLoginError(__UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthenticationRequired(BadLoginError):
|
||||
class AuthenticationRequired(__UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
||||
class AuthManagerError(_UsernameBasedException):
|
||||
class AuthManagerError(__UsernameBasedPasstroughError):
|
||||
pass
|
||||
|
@ -1,6 +1,8 @@
|
||||
import os
|
||||
|
||||
import sys
|
||||
import time
|
||||
import tempfile
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import deluge.configmanager
|
||||
import deluge.log
|
||||
@ -31,3 +33,37 @@ try:
|
||||
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
|
||||
except Exception, e:
|
||||
print e
|
||||
|
||||
def start_core():
|
||||
CWD = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
DAEMON_SCRIPT = """
|
||||
import sys
|
||||
import deluge.main
|
||||
|
||||
sys.argv.extend(['-d', '-c', '%s', '-L', 'info'])
|
||||
|
||||
deluge.main.start_daemon()
|
||||
"""
|
||||
config_directory = set_tmp_config_dir()
|
||||
fp = tempfile.TemporaryFile()
|
||||
fp.write(DAEMON_SCRIPT % config_directory)
|
||||
fp.seek(0)
|
||||
|
||||
core = Popen([sys.executable], cwd=CWD, stdin=fp, stdout=PIPE, stderr=PIPE)
|
||||
while True:
|
||||
line = core.stderr.readline()
|
||||
if "Factory starting on 58846" in line:
|
||||
time.sleep(0.3) # Slight pause just incase
|
||||
break
|
||||
elif 'Traceback' in line:
|
||||
raise SystemExit(
|
||||
"Failed to start core daemon. Do \"\"\"%s\"\"\" to see what's "
|
||||
"happening" %
|
||||
"python -c \"import sys; import tempfile; "
|
||||
"config_directory = tempfile.mkdtemp(); "
|
||||
"import deluge.main; import deluge.configmanager; "
|
||||
"deluge.configmanager.set_config_dir(config_directory); "
|
||||
"sys.argv.extend(['-d', '-c', config_directory, '-L', 'info']); "
|
||||
"deluge.main.start_daemon()"
|
||||
)
|
||||
return core
|
||||
|
@ -2,7 +2,7 @@ from twisted.trial import unittest
|
||||
|
||||
import common
|
||||
|
||||
from deluge.core.authmanager import AuthManager
|
||||
from deluge.core.authmanager import AuthManager, AUTH_LEVEL_ADMIN
|
||||
|
||||
class AuthManagerTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@ -11,4 +11,7 @@ class AuthManagerTestCase(unittest.TestCase):
|
||||
|
||||
def test_authorize(self):
|
||||
from deluge.ui import common
|
||||
self.assertEquals(self.auth.authorize(*common.get_localhost_auth()), 10)
|
||||
self.assertEquals(
|
||||
self.auth.authorize(*common.get_localhost_auth()),
|
||||
AUTH_LEVEL_ADMIN
|
||||
)
|
||||
|
@ -1,53 +1,80 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import tempfile
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import common
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from deluge import error
|
||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT
|
||||
from deluge.ui.client import client
|
||||
|
||||
CWD = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
DAEMON_SCRIPT = """
|
||||
import sys
|
||||
import deluge.main
|
||||
|
||||
sys.argv.extend(['-d', '-c', '%s', '-Linfo'])
|
||||
|
||||
deluge.main.start_daemon()
|
||||
"""
|
||||
|
||||
class ClientTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
config_directory = common.set_tmp_config_dir()
|
||||
|
||||
fp = tempfile.TemporaryFile()
|
||||
fp.write(DAEMON_SCRIPT % config_directory)
|
||||
fp.seek(0)
|
||||
|
||||
self.core = Popen([sys.executable], cwd=CWD,
|
||||
stdin=fp, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
time.sleep(2) # Slight pause just incase
|
||||
self.core = common.start_core()
|
||||
|
||||
def tearDown(self):
|
||||
self.core.terminate()
|
||||
|
||||
def test_connect_no_credentials(self):
|
||||
return # hack whilst core is broken
|
||||
|
||||
d = client.connect("localhost", 58846)
|
||||
d.addCallback(self.assertEquals, 10)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.AuthenticationRequired),
|
||||
error.AuthenticationRequired
|
||||
)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
def test_connect_localclient(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username, password=password
|
||||
)
|
||||
|
||||
def on_connect(result):
|
||||
self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN)
|
||||
self.addCleanup(client.disconnect)
|
||||
return result
|
||||
|
||||
d.addCallback(on_connect)
|
||||
return d
|
||||
|
||||
def test_connect_bad_password(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username, password=password+'1'
|
||||
)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.BadLoginError),
|
||||
error.BadLoginError
|
||||
)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
||||
def test_connect_without_password(self):
|
||||
from deluge.ui import common
|
||||
username, password = common.get_localhost_auth()
|
||||
d = client.connect(
|
||||
"localhost", 58846, username=username
|
||||
)
|
||||
|
||||
def on_failure(failure):
|
||||
self.assertEqual(
|
||||
failure.trap(error.AuthenticationRequired),
|
||||
error.AuthenticationRequired
|
||||
)
|
||||
self.assertEqual(failure.value.username, username)
|
||||
self.addCleanup(client.disconnect)
|
||||
|
||||
d.addErrback(on_failure)
|
||||
return d
|
||||
|
@ -13,11 +13,14 @@ except ImportError:
|
||||
|
||||
import os
|
||||
import common
|
||||
import warnings
|
||||
rpath = common.rpath
|
||||
|
||||
from deluge.core.rpcserver import RPCServer
|
||||
from deluge.core.core import Core
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
from deluge.ui.web.common import compress
|
||||
warnings.resetwarnings()
|
||||
import deluge.component as component
|
||||
import deluge.error
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import reactor
|
||||
@ -9,7 +10,10 @@ from twisted.web.server import Site
|
||||
|
||||
from deluge.httpdownloader import download_file
|
||||
from deluge.log import setupLogger
|
||||
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
from deluge.ui.web.common import compress
|
||||
warnings.resetwarnings()
|
||||
|
||||
from email.utils import formatdate
|
||||
|
||||
|
@ -45,8 +45,8 @@ except ImportError:
|
||||
import zlib
|
||||
|
||||
import deluge.common
|
||||
from deluge import error
|
||||
from deluge.log import LOG as log
|
||||
from deluge.error import AuthenticationRequired
|
||||
from deluge.event import known_events
|
||||
|
||||
if deluge.common.windows_check():
|
||||
@ -206,7 +206,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
# Run the callbacks registered with this Deferred object
|
||||
d.callback(request[2])
|
||||
elif message_type == RPC_EVENT_AUTH:
|
||||
d.errback(AuthenticationRequired(request[2], request[3]))
|
||||
# Recreate exception and errback'it
|
||||
d.errback(getattr(error, request[2])(*request[3], **request[4]))
|
||||
elif message_type == RPC_ERROR:
|
||||
# Create the DelugeRPCError to pass to the errback
|
||||
r = self.__rpc_requests[request_id]
|
||||
@ -416,13 +417,16 @@ class DaemonSSLProxy(DaemonProxy):
|
||||
containing a `:class:DelugeRPCError` object.
|
||||
"""
|
||||
try:
|
||||
if error_data.check(AuthenticationRequired):
|
||||
if isinstance(error_data.value, error.NotAuthorizedError):
|
||||
# Still log these errors
|
||||
log.error(error_data.value.logable())
|
||||
return error_data
|
||||
if isinstance(error_data.value, error.__PassthroughError):
|
||||
return error_data
|
||||
except:
|
||||
pass
|
||||
|
||||
if error_data.value.exception_type != 'AuthManagerError':
|
||||
log.error(error_data.value.logable())
|
||||
log.error(error_data.value.logable())
|
||||
return error_data
|
||||
|
||||
def __on_connect(self, result):
|
||||
|
@ -43,6 +43,7 @@ from twisted.internet import reactor
|
||||
import deluge.component as component
|
||||
import common
|
||||
import deluge.configmanager
|
||||
from deluge.ui.common import get_localhost_auth
|
||||
from deluge.ui.client import client
|
||||
import deluge.ui.client
|
||||
from deluge.configmanager import ConfigManager
|
||||
@ -100,6 +101,7 @@ class ConnectionManager(component.Component):
|
||||
self.gtkui_config = ConfigManager("gtkui.conf")
|
||||
|
||||
self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG)
|
||||
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
|
||||
|
||||
self.running = False
|
||||
|
||||
@ -483,8 +485,9 @@ class ConnectionManager(component.Component):
|
||||
dialog.get_password())
|
||||
d = dialog.run().addCallback(dialog_finished, host, port, user)
|
||||
return d
|
||||
dialogs.ErrorDialog(_("Failed To Authenticate"),
|
||||
reason.value.exception_msg).run()
|
||||
dialogs.ErrorDialog(
|
||||
_("Failed To Authenticate"), reason.value.message
|
||||
).run()
|
||||
|
||||
def on_button_connect_clicked(self, widget=None):
|
||||
model, row = self.hostlist.get_selection().get_selected()
|
||||
@ -683,3 +686,16 @@ class ConnectionManager(component.Component):
|
||||
|
||||
def on_askpassword_dialog_entry_activate(self, entry):
|
||||
self.askpassword_dialog.response(gtk.RESPONSE_OK)
|
||||
|
||||
def __migrate_config_1_to_2(self, config):
|
||||
localclient_username, localclient_password = get_localhost_auth()
|
||||
if not localclient_username:
|
||||
# Nothing to do here, there's no auth file
|
||||
return
|
||||
for idx, (_, host, _, username, _) in enumerate(config["hosts"][:]):
|
||||
if host in ("127.0.0.1", "localhost"):
|
||||
if not username:
|
||||
config["hosts"][idx][3] = localclient_username
|
||||
config["hosts"][idx][4] = localclient_password
|
||||
return config
|
||||
|
||||
|
@ -35,7 +35,6 @@
|
||||
|
||||
import zlib
|
||||
import gettext
|
||||
from mako.template import Template as MakoTemplate
|
||||
from deluge import common
|
||||
|
||||
_ = lambda x: gettext.gettext(x).decode("utf-8")
|
||||
@ -59,18 +58,31 @@ def compress(contents, request):
|
||||
contents += compress.flush()
|
||||
return contents
|
||||
|
||||
class Template(MakoTemplate):
|
||||
"""
|
||||
A template that adds some built-ins to the rendering
|
||||
"""
|
||||
|
||||
builtins = {
|
||||
"_": _,
|
||||
"escape": escape,
|
||||
"version": common.get_version()
|
||||
}
|
||||
|
||||
def render(self, *args, **data):
|
||||
data.update(self.builtins)
|
||||
rendered = MakoTemplate.render_unicode(self, *args, **data)
|
||||
return rendered.encode('utf-8', 'replace')
|
||||
try:
|
||||
# This is beeing done like this in order to allow tests to use the above
|
||||
# `compress` without requiring Mako to be instaled
|
||||
from mako.template import Template as MakoTemplate
|
||||
class Template(MakoTemplate):
|
||||
"""
|
||||
A template that adds some built-ins to the rendering
|
||||
"""
|
||||
|
||||
builtins = {
|
||||
"_": _,
|
||||
"escape": escape,
|
||||
"version": common.get_version()
|
||||
}
|
||||
|
||||
def render(self, *args, **data):
|
||||
data.update(self.builtins)
|
||||
rendered = MakoTemplate.render_unicode(self, *args, **data)
|
||||
return rendered.encode('utf-8', 'replace')
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn("The Mako library is required to run deluge.ui.web",
|
||||
RuntimeWarning)
|
||||
class Template(object):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"The Mako library is required to run deluge.ui.web"
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user