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:
Pedro Algarvio 2011-04-27 19:28:16 +01:00
parent bb9a8509c8
commit f41f6ad46a
11 changed files with 215 additions and 75 deletions

View File

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

View File

@ -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,9 +265,14 @@ 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:
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:
@ -273,7 +281,7 @@ class DelugeRPCProtocol(Protocol):
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,12 +417,15 @@ 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())
return error_data

View File

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

View File

@ -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,6 +58,10 @@ def compress(contents, request):
contents += compress.flush()
return contents
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
@ -74,3 +77,12 @@ class Template(MakoTemplate):
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"
)