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
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 * 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 plugin, which is also shipped with Deluge, and it does a better job and
now, it even supports multiple users perfectly. now, it even supports multiple users perfectly.
* Authentication/Permission exceptions are now sent to clients and recreated
there to allow acting upon them.
==== Core ==== ==== Core ====
* Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent * 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, * 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 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. 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 ==== ==== GtkUI ====
* Fix uncaught exception when closing deluge in classic mode * Fix uncaught exception when closing deluge in classic mode
* Allow changing ownership of torrents * 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 ==== ==== WebUI ====
* Migrate to ExtJS 3.1 * Migrate to ExtJS 3.1

View file

@ -56,7 +56,7 @@ except ImportError:
import deluge.component as component import deluge.component as component
import deluge.configmanager import deluge.configmanager
from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_ADMIN 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_RESPONSE = 1
RPC_ERROR = 2 RPC_ERROR = 2
@ -219,6 +219,9 @@ class DelugeRPCProtocol(Protocol):
log.info("Deluge client disconnected: %s", reason.value) 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): def dispatch(self, request_id, method, args, kwargs):
""" """
This method is run when a RPC Request is made. It will run the local method 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: if ret:
self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0]) self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self 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: 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() sendError()
log.exception(e) log.exception(e)
else: else:
@ -273,7 +281,7 @@ class DelugeRPCProtocol(Protocol):
self.transport.loseConnection() self.transport.loseConnection()
finally: finally:
return 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") log.debug("RPC dispatch daemon.set_event_interest")
# This special case is to allow clients to set which events they are # This special case is to allow clients to set which events they are
# interested in receiving. # interested in receiving.
@ -289,22 +297,24 @@ class DelugeRPCProtocol(Protocol):
finally: finally:
return 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) log.debug("RPC dispatch %s", method)
try: try:
method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level
auth_level = self.factory.authorized_sessions[self.transport.sessionno][0] auth_level = self.factory.authorized_sessions[self.transport.sessionno][0]
if auth_level < method_auth_requirement: if auth_level < method_auth_requirement:
# This session is not allowed to call this method # 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) log.debug("Session %s is trying to call a method it is not "
raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement)) "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 # Set the session_id in the factory so that methods can know
# which session is calling it. # which session is calling it.
self.factory.session_id = self.transport.sessionno self.factory.session_id = self.transport.sessionno
ret = self.factory.methods[method](*args, **kwargs) ret = self.factory.methods[method](*args, **kwargs)
except Exception, e: except Exception, e:
sendError() 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): if not isinstance(e, DelugeError):
log.exception("Exception calling RPC request: %s", e) log.exception("Exception calling RPC request: %s", e)
else: else:
@ -545,8 +555,12 @@ def generate_ssl_keys():
# Write out files # Write out files
ssl_dir = deluge.configmanager.get_config_dir("ssl") 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.pkey"), "w").write(
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) 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 # Make the files only readable by this user
for f in ("daemon.pkey", "daemon.cert"): for f in ("daemon.pkey", "daemon.cert"):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE) 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): class InvalidPathError(DelugeError):
pass pass
class NotAuthorizedError(DelugeError): class __PassthroughError(DelugeError):
pass 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): def _get_message(self):
return self._message return self._message
@ -71,16 +84,16 @@ class _UsernameBasedException(DelugeError):
del _get_username, _set_username del _get_username, _set_username
def __init__(self, message, username): def __init__(self, message, username):
super(_UsernameBasedException, self).__init__(message) super(__UsernameBasedPasstroughError, self).__init__(message)
self.message = message self.message = message
self.username = username self.username = username
class BadLoginError(_UsernameBasedException): class BadLoginError(__UsernameBasedPasstroughError):
pass pass
class AuthenticationRequired(BadLoginError): class AuthenticationRequired(__UsernameBasedPasstroughError):
pass pass
class AuthManagerError(_UsernameBasedException): class AuthManagerError(__UsernameBasedPasstroughError):
pass pass

View file

@ -1,6 +1,8 @@
import os import os
import sys
import time
import tempfile import tempfile
from subprocess import Popen, PIPE
import deluge.configmanager import deluge.configmanager
import deluge.log import deluge.log
@ -31,3 +33,37 @@ try:
gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n")) gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n"))
except Exception, e: except Exception, e:
print 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 import common
from deluge.core.authmanager import AuthManager from deluge.core.authmanager import AuthManager, AUTH_LEVEL_ADMIN
class AuthManagerTestCase(unittest.TestCase): class AuthManagerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
@ -11,4 +11,7 @@ class AuthManagerTestCase(unittest.TestCase):
def test_authorize(self): def test_authorize(self):
from deluge.ui import common 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 import common
from twisted.trial import unittest 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 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): class ClientTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
config_directory = common.set_tmp_config_dir() self.core = common.start_core()
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
def tearDown(self): def tearDown(self):
self.core.terminate() self.core.terminate()
def test_connect_no_credentials(self): def test_connect_no_credentials(self):
return # hack whilst core is broken
d = client.connect("localhost", 58846) 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): def on_connect(result):
self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN)
self.addCleanup(client.disconnect) self.addCleanup(client.disconnect)
return result return result
d.addCallback(on_connect) d.addCallback(on_connect)
return d 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 os
import common import common
import warnings
rpath = common.rpath rpath = common.rpath
from deluge.core.rpcserver import RPCServer from deluge.core.rpcserver import RPCServer
from deluge.core.core import Core from deluge.core.core import Core
warnings.filterwarnings("ignore", category=RuntimeWarning)
from deluge.ui.web.common import compress from deluge.ui.web.common import compress
warnings.resetwarnings()
import deluge.component as component import deluge.component as component
import deluge.error import deluge.error

View file

@ -1,4 +1,5 @@
import os import os
import warnings
from twisted.trial import unittest from twisted.trial import unittest
from twisted.internet import reactor from twisted.internet import reactor
@ -9,7 +10,10 @@ from twisted.web.server import Site
from deluge.httpdownloader import download_file from deluge.httpdownloader import download_file
from deluge.log import setupLogger from deluge.log import setupLogger
warnings.filterwarnings("ignore", category=RuntimeWarning)
from deluge.ui.web.common import compress from deluge.ui.web.common import compress
warnings.resetwarnings()
from email.utils import formatdate from email.utils import formatdate

View file

@ -45,8 +45,8 @@ except ImportError:
import zlib import zlib
import deluge.common import deluge.common
from deluge import error
from deluge.log import LOG as log from deluge.log import LOG as log
from deluge.error import AuthenticationRequired
from deluge.event import known_events from deluge.event import known_events
if deluge.common.windows_check(): if deluge.common.windows_check():
@ -206,7 +206,8 @@ class DelugeRPCProtocol(Protocol):
# Run the callbacks registered with this Deferred object # Run the callbacks registered with this Deferred object
d.callback(request[2]) d.callback(request[2])
elif message_type == RPC_EVENT_AUTH: 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: elif message_type == RPC_ERROR:
# Create the DelugeRPCError to pass to the errback # Create the DelugeRPCError to pass to the errback
r = self.__rpc_requests[request_id] r = self.__rpc_requests[request_id]
@ -416,12 +417,15 @@ class DaemonSSLProxy(DaemonProxy):
containing a `:class:DelugeRPCError` object. containing a `:class:DelugeRPCError` object.
""" """
try: 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 return error_data
except: except:
pass pass
if error_data.value.exception_type != 'AuthManagerError':
log.error(error_data.value.logable()) log.error(error_data.value.logable())
return error_data return error_data

View file

@ -43,6 +43,7 @@ from twisted.internet import reactor
import deluge.component as component import deluge.component as component
import common import common
import deluge.configmanager import deluge.configmanager
from deluge.ui.common import get_localhost_auth
from deluge.ui.client import client from deluge.ui.client import client
import deluge.ui.client import deluge.ui.client
from deluge.configmanager import ConfigManager from deluge.configmanager import ConfigManager
@ -100,6 +101,7 @@ class ConnectionManager(component.Component):
self.gtkui_config = ConfigManager("gtkui.conf") self.gtkui_config = ConfigManager("gtkui.conf")
self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) 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 self.running = False
@ -483,8 +485,9 @@ class ConnectionManager(component.Component):
dialog.get_password()) dialog.get_password())
d = dialog.run().addCallback(dialog_finished, host, port, user) d = dialog.run().addCallback(dialog_finished, host, port, user)
return d return d
dialogs.ErrorDialog(_("Failed To Authenticate"), dialogs.ErrorDialog(
reason.value.exception_msg).run() _("Failed To Authenticate"), reason.value.message
).run()
def on_button_connect_clicked(self, widget=None): def on_button_connect_clicked(self, widget=None):
model, row = self.hostlist.get_selection().get_selected() model, row = self.hostlist.get_selection().get_selected()
@ -683,3 +686,16 @@ class ConnectionManager(component.Component):
def on_askpassword_dialog_entry_activate(self, entry): def on_askpassword_dialog_entry_activate(self, entry):
self.askpassword_dialog.response(gtk.RESPONSE_OK) 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 zlib
import gettext import gettext
from mako.template import Template as MakoTemplate
from deluge import common from deluge import common
_ = lambda x: gettext.gettext(x).decode("utf-8") _ = lambda x: gettext.gettext(x).decode("utf-8")
@ -59,7 +58,11 @@ def compress(contents, request):
contents += compress.flush() contents += compress.flush()
return contents return contents
class Template(MakoTemplate): 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 A template that adds some built-ins to the rendering
""" """
@ -74,3 +77,12 @@ class Template(MakoTemplate):
data.update(self.builtins) data.update(self.builtins)
rendered = MakoTemplate.render_unicode(self, *args, **data) rendered = MakoTemplate.render_unicode(self, *args, **data)
return rendered.encode('utf-8', 'replace') 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"
)