summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--deluge/tests/common.py6
-rw-r--r--deluge/tests/test_client.py53
-rw-r--r--deluge/tests/test_json_api.py277
-rw-r--r--deluge/tests/test_web_api.py223
-rw-r--r--deluge/ui/web/auth.py41
-rw-r--r--deluge/ui/web/json_api.py174
-rw-r--r--deluge/ui/web/server.py21
-rw-r--r--setup.cfg2
-rw-r--r--tox.ini1
9 files changed, 634 insertions, 164 deletions
diff --git a/deluge/tests/common.py b/deluge/tests/common.py
index bf7aedf4d..80e31b757 100644
--- a/deluge/tests/common.py
+++ b/deluge/tests/common.py
@@ -86,10 +86,10 @@ class ProcessOutputHandler(protocol.ProcessProtocol):
else:
self.quit_d.errback(status)
- def check_callbacks(self, data, type="stdout"):
+ def check_callbacks(self, data, cb_type="stdout"):
ret = False
for c in self.callbacks:
- if type not in c["types"] or c["deferred"].called:
+ if cb_type not in c["types"] or c["deferred"].called:
continue
for trigger in c["triggers"]:
if trigger["expr"] in data:
@@ -118,7 +118,7 @@ class ProcessOutputHandler(protocol.ProcessProtocol):
"""Process output from stderr"""
self.log_output += data
self.stderr_out += data
- self.check_callbacks(data, type="stderr")
+ self.check_callbacks(data, cb_type="stderr")
if not self.print_stderr:
return
data = "\n%s" % data.strip()
diff --git a/deluge/tests/test_client.py b/deluge/tests/test_client.py
index f2cfd28cd..fe4d7ed65 100644
--- a/deluge/tests/test_client.py
+++ b/deluge/tests/test_client.py
@@ -1,3 +1,10 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
from twisted.internet import defer
import deluge.component as component
@@ -78,9 +85,7 @@ class ClientTestCase(BaseTestCase, DaemonBase):
return d
def test_connect_no_credentials(self):
- d = client.connect(
- "localhost", self.listen_port, username="", password=""
- )
+ d = client.connect("localhost", self.listen_port, username="", password="")
def on_connect(result):
self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN)
@@ -92,9 +97,7 @@ class ClientTestCase(BaseTestCase, DaemonBase):
def test_connect_localclient(self):
username, password = deluge.ui.common.get_localhost_auth()
- d = client.connect(
- "localhost", self.listen_port, username=username, password=password
- )
+ d = client.connect("localhost", self.listen_port, username=username, password=password)
def on_connect(result):
self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN)
@@ -106,15 +109,29 @@ class ClientTestCase(BaseTestCase, DaemonBase):
def test_connect_bad_password(self):
username, password = deluge.ui.common.get_localhost_auth()
- d = client.connect(
- "localhost", self.listen_port, username=username, password=password + "1"
- )
+ d = client.connect("localhost", self.listen_port, username=username, password=password + "1")
+
+ def on_failure(failure):
+ self.assertEqual(
+ failure.trap(error.BadLoginError),
+ error.BadLoginError
+ )
+ self.assertEquals(failure.value.message, "Password does not match")
+ self.addCleanup(client.disconnect)
+
+ d.addCallbacks(self.fail, on_failure)
+ return d
+
+ def test_connect_invalid_user(self):
+ username, password = deluge.ui.common.get_localhost_auth()
+ d = client.connect("localhost", self.listen_port, username="invalid-user")
def on_failure(failure):
self.assertEqual(
failure.trap(error.BadLoginError),
error.BadLoginError
)
+ self.assertEquals(failure.value.message, "Username does not exist")
self.addCleanup(client.disconnect)
d.addCallbacks(self.fail, on_failure)
@@ -122,9 +139,7 @@ class ClientTestCase(BaseTestCase, DaemonBase):
def test_connect_without_password(self):
username, password = deluge.ui.common.get_localhost_auth()
- d = client.connect(
- "localhost", self.listen_port, username=username
- )
+ d = client.connect("localhost", self.listen_port, username=username)
def on_failure(failure):
self.assertEqual(
@@ -138,10 +153,18 @@ class ClientTestCase(BaseTestCase, DaemonBase):
return d
@defer.inlineCallbacks
+ def test_connect_with_password(self):
+ username, password = deluge.ui.common.get_localhost_auth()
+ yield client.connect("localhost", self.listen_port, username=username, password=password)
+ yield client.core.create_account("testuser", "testpw", "DEFAULT")
+ yield client.disconnect()
+ ret = yield client.connect("localhost", self.listen_port, username="testuser", password="testpw")
+ self.assertEquals(ret, deluge.common.AUTH_LEVEL_NORMAL)
+ yield
+
+ @defer.inlineCallbacks
def test_invalid_rpc_method_call(self):
- yield client.connect(
- "localhost", self.listen_port, username="", password=""
- )
+ yield client.connect("localhost", self.listen_port, username="", password="")
d = client.core.invalid_method()
def on_failure(failure):
diff --git a/deluge/tests/test_json_api.py b/deluge/tests/test_json_api.py
new file mode 100644
index 000000000..76e9b702e
--- /dev/null
+++ b/deluge/tests/test_json_api.py
@@ -0,0 +1,277 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import json as json_lib
+
+from mock import MagicMock
+from twisted.internet import defer
+from twisted.web import server
+from twisted.web.http import Request
+
+import deluge.common
+import deluge.component as component
+import deluge.ui.web.auth
+import deluge.ui.web.json_api
+from deluge.error import DelugeError
+from deluge.ui.client import client
+from deluge.ui.web.auth import Auth
+from deluge.ui.web.json_api import JSON, JSONException
+
+from . import common
+from .basetest import BaseTestCase
+from .daemon_base import DaemonBase
+
+common.disable_new_release_check()
+
+
+class JSONBase(BaseTestCase, DaemonBase):
+
+ def connect_client(self, *args, **kwargs):
+ return client.connect(
+ "localhost", self.listen_port, username=kwargs.get("user", ""),
+ password=kwargs.get("password", "")
+ )
+
+ def disconnect_client(self, *args):
+ return client.disconnect()
+
+ def tear_down(self):
+ d = component.shutdown()
+ d.addCallback(self.disconnect_client)
+ d.addCallback(self.terminate_core)
+ return d
+
+
+class JSONTestCase(JSONBase):
+
+ def set_up(self):
+ d = self.common_set_up()
+ d.addCallback(self.start_core)
+ d.addCallbacks(self.connect_client, self.terminate_core)
+ return d
+
+ @defer.inlineCallbacks
+ def test_get_remote_methods(self):
+ json = JSON()
+ methods = yield json.get_remote_methods()
+ self.assertEquals(type(methods), tuple)
+ self.assertTrue(len(methods) > 0)
+
+ def test_render_fail_disconnected(self):
+ json = JSON()
+ request = MagicMock()
+ request.method = "POST"
+ request._disconnected = True
+ # When disconnected, returns empty string
+ self.assertEquals(json.render(request), "")
+
+ def test_render_fail(self):
+ json = JSON()
+ request = MagicMock()
+ request.method = "POST"
+
+ def compress(contents, request):
+ return contents
+ self.patch(deluge.ui.web.json_api, "compress", compress)
+
+ def write(response_str):
+ request.write_was_called = True
+ response = json_lib.loads(response_str)
+ self.assertEquals(response["result"], None)
+ self.assertEquals(response["id"], None)
+ self.assertEquals(response["error"]["message"], "JSONException: JSON not decodable")
+ self.assertEquals(response["error"]["code"], 5)
+
+ request.write = write
+ request.write_was_called = False
+ request._disconnected = False
+ self.assertEquals(json.render(request), server.NOT_DONE_YET)
+ self.assertTrue(request.write_was_called)
+
+ @defer.inlineCallbacks
+ def test_handle_request_invalid_method(self):
+ json = JSON()
+ request = MagicMock()
+ json_data = {"method": "no-existing-module.test", "id": 0, "params": []}
+ request.json = json_lib.dumps(json_data)
+ request_id, result, error = json._handle_request(request)
+ self.assertEquals(error, {'message': 'Unknown method', 'code': 2})
+ yield
+ return
+
+ @defer.inlineCallbacks
+ def test_handle_request_invalid_json_request(self):
+ json = JSON()
+ request = MagicMock()
+ request.json = json_lib.dumps({"id": 0, "params": []})
+ self.assertRaises(JSONException, json._handle_request, request)
+ request.json = json_lib.dumps({"method": "some.method", "params": []})
+ self.assertRaises(JSONException, json._handle_request, request)
+ request.json = json_lib.dumps({"method": "some.method", "id": 0})
+ self.assertRaises(JSONException, json._handle_request, request)
+ yield
+
+
+class JSONCustomUserTestCase(JSONBase):
+
+ def set_up(self):
+ d = self.common_set_up()
+ d.addCallback(self.start_core)
+ return d
+
+ @defer.inlineCallbacks
+ def test_handle_request_auth_error(self):
+ yield self.connect_client()
+ json = JSON()
+ auth_conf = {"session_timeout": 10, "sessions": []}
+ Auth(auth_conf) # Must create the component
+
+ # Must be called to update remote methods in json object
+ yield json.get_remote_methods()
+
+ request = MagicMock()
+ request.getCookie = MagicMock(return_value="bad_value")
+ json_data = {"method": "core.get_libtorrent_version", "id": 0, "params": []}
+ request.json = json_lib.dumps(json_data)
+ request_id, result, error = json._handle_request(request)
+ self.assertEquals(error, {'message': 'Not authenticated', 'code': 1})
+ return
+
+
+class RPCRaiseDelugeErrorJSONTestCase(JSONBase):
+
+ def set_up(self):
+ d = self.common_set_up()
+ custom_script = """
+ from deluge.error import DelugeError
+ from deluge.core.rpcserver import export
+ class TestClass(object):
+ @export()
+ def test(self):
+ raise DelugeError("DelugeERROR")
+
+ test = TestClass()
+ daemon.rpcserver.register_object(test)
+"""
+ d.addCallback(self.start_core, custom_script=custom_script)
+ d.addCallbacks(self.connect_client, self.terminate_core)
+ return d
+
+ @defer.inlineCallbacks
+ def test_handle_request_method_raise_delugeerror(self):
+ json = JSON()
+
+ def get_session_id(s_id):
+ return s_id
+ self.patch(deluge.ui.web.auth, "get_session_id", get_session_id)
+ auth_conf = {"session_timeout": 10, "sessions": []}
+ auth = Auth(auth_conf)
+ request = Request(MagicMock(), False)
+ request.base = ""
+ auth._create_session(request)
+ methods = yield json.get_remote_methods()
+ # Verify the function has been registered
+ self.assertTrue("testclass.test" in methods)
+
+ request = MagicMock()
+ request.getCookie = MagicMock(return_value=auth.config["sessions"].keys()[0])
+ json_data = {"method": "testclass.test", "id": 0, "params": []}
+ request.json = json_lib.dumps(json_data)
+ request_id, result, error = json._handle_request(request)
+ result.addCallback(self.fail)
+
+ def on_error(error):
+ self.assertEquals(error.type, DelugeError)
+ result.addErrback(on_error)
+ yield
+ return
+
+
+class JSONRequestFailedTestCase(JSONBase):
+
+ def set_up(self):
+ d = self.common_set_up()
+ custom_script = """
+ from deluge.error import DelugeError
+ from deluge.core.rpcserver import export
+ from twisted.internet import reactor, task
+ class TestClass(object):
+ @export()
+ def test(self):
+ def test_raise_error():
+ raise DelugeError("DelugeERROR")
+
+ return task.deferLater(reactor, 1, test_raise_error)
+
+ test = TestClass()
+ daemon.rpcserver.register_object(test)
+"""
+ from twisted.internet.defer import Deferred
+ extra_callback = {"deferred": Deferred(), "types": ["stderr"],
+ "timeout": 10,
+ "triggers": [{"expr": "in test_raise_error",
+ "value": lambda reader, data, data_all: "Test"}]}
+
+ def on_test_raise(*args):
+ self.assertTrue("in test_raise_error" in self.core.stderr_out)
+
+ extra_callback["deferred"].addCallback(on_test_raise)
+ self.d_test_raise_error_log = extra_callback["deferred"]
+
+ d.addCallback(self.start_core, custom_script=custom_script, print_stderr=False,
+ timeout=5, extra_callbacks=[extra_callback])
+ d.addCallbacks(self.connect_client, self.terminate_core)
+ return d
+
+ @defer.inlineCallbacks
+ def test_render_on_rpc_request_failed(self):
+ json = JSON()
+
+ def get_session_id(s_id):
+ return s_id
+ self.patch(deluge.ui.web.auth, "get_session_id", get_session_id)
+ auth_conf = {"session_timeout": 10, "sessions": []}
+ auth = Auth(auth_conf)
+ request = Request(MagicMock(), False)
+ request.base = ""
+ auth._create_session(request)
+ methods = yield json.get_remote_methods()
+ # Verify the function has been registered
+ self.assertTrue("testclass.test" in methods)
+
+ request = MagicMock()
+ request.getCookie = MagicMock(return_value=auth.config["sessions"].keys()[0])
+
+ def compress(contents, request):
+ return contents
+ # Patch compress to avoid having to decompress output
+ self.patch(deluge.ui.web.json_api, "compress", compress)
+
+ def write(response_str):
+ request.write_was_called = True
+ response = json_lib.loads(response_str)
+ self.assertEquals(response["result"], None, "BAD RESULT")
+ self.assertEquals(response["id"], 0)
+ self.assertEquals(response["error"]["message"],
+ "Failure: [Failure instance: Traceback (failure with no frames):"
+ " <class 'deluge.error.DelugeError'>: DelugeERROR\n]"),
+ self.assertEquals(response["error"]["code"], 4)
+
+ request.write = write
+ request.write_was_called = False
+ request._disconnected = False
+ json_data = {"method": "testclass.test", "id": 0, "params": []}
+ request.json = json_lib.dumps(json_data)
+ d = json._on_json_request(request)
+
+ def on_success(arg):
+ self.assertEquals(arg, server.NOT_DONE_YET)
+ return True
+ d.addCallbacks(on_success, self.fail)
+ yield d
diff --git a/deluge/tests/test_web_api.py b/deluge/tests/test_web_api.py
new file mode 100644
index 000000000..8dfd3c8c5
--- /dev/null
+++ b/deluge/tests/test_web_api.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2016 bendikro <bro.devel+deluge@gmail.com>
+#
+# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
+# the additional special exception to link portions of this program with the OpenSSL library.
+# See LICENSE for more details.
+#
+
+import os
+from StringIO import StringIO
+
+from twisted.internet import defer, reactor
+from twisted.python.failure import Failure
+from twisted.web.client import Agent, FileBodyProducer
+from twisted.web.http_headers import Headers
+from twisted.web.static import File
+
+import deluge.common
+import deluge.component as component
+import deluge.ui.web.auth
+import deluge.ui.web.server
+from deluge import configmanager
+from deluge.ui.client import client
+from deluge.ui.web.server import DelugeWeb
+
+from . import common
+from .basetest import BaseTestCase
+from .daemon_base import DaemonBase
+
+common.disable_new_release_check()
+
+
+class ReactorOverride(object):
+
+ def __getattr__(self, attr):
+ if attr == "run":
+ return self._run
+ if attr == "stop":
+ return self._stop
+ return getattr(reactor, attr)
+
+ def _run(self):
+ pass
+
+ def _stop(self):
+ pass
+
+
+class WebAPITestCase(BaseTestCase, DaemonBase):
+
+ def set_up(self):
+ self.host_id = None
+ deluge.ui.web.server.reactor = ReactorOverride()
+ d = self.common_set_up()
+ d.addCallback(self.start_core)
+ d.addCallback(self.start_webapi)
+ return d
+
+ def start_webapi(self, arg):
+ self.webserver_listen_port = 8999
+
+ config_defaults = deluge.ui.web.server.CONFIG_DEFAULTS.copy()
+ config_defaults["port"] = self.webserver_listen_port
+ self.config = configmanager.ConfigManager("web.conf", config_defaults)
+
+ self.deluge_web = DelugeWeb()
+
+ host = list(self.deluge_web.web_api.host_list["hosts"][0])
+ host[2] = self.listen_port
+ self.deluge_web.web_api.host_list["hosts"][0] = tuple(host)
+ self.host_id = host[0]
+ self.deluge_web.start()
+
+ def tear_down(self):
+ d = component.shutdown()
+ d.addCallback(self.terminate_core)
+ return d
+
+ def test_connect_invalid_host(self):
+ d = self.deluge_web.web_api.connect("id")
+ d.addCallback(self.fail)
+ d.addErrback(self.assertIsInstance, Failure)
+ return d
+
+ def test_connect(self):
+ d = self.deluge_web.web_api.connect(self.host_id)
+
+ def on_connect(result):
+ self.assertEquals(type(result), tuple)
+ self.assertTrue(len(result) > 0)
+ self.addCleanup(client.disconnect)
+ return result
+
+ d.addCallback(on_connect)
+ d.addErrback(self.fail)
+ return d
+
+ def test_disconnect(self):
+ d = self.deluge_web.web_api.connect(self.host_id)
+
+ @defer.inlineCallbacks
+ def on_connect(result):
+ self.assertTrue(self.deluge_web.web_api.connected())
+ yield self.deluge_web.web_api.disconnect()
+ self.assertFalse(self.deluge_web.web_api.connected())
+
+ d.addCallback(on_connect)
+ d.addErrback(self.fail)
+ return d
+
+ def test_get_config(self):
+ config = self.deluge_web.web_api.get_config()
+ self.assertEquals(self.webserver_listen_port, config["port"])
+
+ def test_set_config(self):
+ config = self.deluge_web.web_api.get_config()
+ config["pwd_salt"] = "new_salt"
+ config["pwd_sha1"] = 'new_sha'
+ config["sessions"] = {
+ "233f23632af0a74748bc5dd1d8717564748877baa16420e6898e17e8aa365e6e": {
+ "login": "skrot",
+ "expires": 1460030877.0,
+ "level": 10
+ }
+ }
+ self.deluge_web.web_api.set_config(config)
+ web_config = component.get("DelugeWeb").config.config
+ self.assertNotEquals(config["pwd_salt"], web_config["pwd_salt"])
+ self.assertNotEquals(config["pwd_sha1"], web_config["pwd_sha1"])
+ self.assertNotEquals(config["sessions"], web_config["sessions"])
+
+ @defer.inlineCallbacks
+ def get_host_status(self):
+ host = list(self.deluge_web.web_api._get_host(self.host_id))
+ host[3] = 'Online'
+ host[4] = u'2.0.0.dev562'
+ status = yield self.deluge_web.web_api.get_host_status(self.host_id)
+ self.assertEquals(status, tuple(status))
+
+ def test_get_host(self):
+ self.assertEquals(self.deluge_web.web_api._get_host("invalid_id"), None)
+ conn = self.deluge_web.web_api.host_list["hosts"][0]
+ self.assertEquals(self.deluge_web.web_api._get_host(conn[0]), conn)
+
+ def test_add_host(self):
+ conn = [None, '', 0, '', '']
+ self.assertEquals(self.deluge_web.web_api._get_host(conn[0]), None)
+ # Add valid host
+ ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
+ self.assertEquals(ret[0], True)
+ conn[0] = ret[1]
+ self.assertEquals(self.deluge_web.web_api._get_host(conn[0]), conn)
+
+ # Add already existing host
+ ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
+ self.assertEquals(ret, (False, "Host already in the list"))
+
+ # Add invalid port
+ conn[2] = "bad port"
+ ret = self.deluge_web.web_api.add_host(conn[1], conn[2], conn[3], conn[4])
+ self.assertEquals(ret, (False, "Port is invalid"))
+
+ def test_remove_host(self):
+ conn = ['connection_id', '', 0, '', '']
+ self.deluge_web.web_api.host_list["hosts"].append(conn)
+ self.assertEquals(self.deluge_web.web_api._get_host(conn[0]), conn)
+ # Remove valid host
+ self.assertTrue(self.deluge_web.web_api.remove_host(conn[0]))
+ self.assertEquals(self.deluge_web.web_api._get_host(conn[0]), None)
+ # Remove non-existing host
+ self.assertFalse(self.deluge_web.web_api.remove_host(conn[0]))
+
+ def test_get_torrent_info(self):
+ filename = os.path.join(os.path.dirname(__file__), "test.torrent")
+ ret = self.deluge_web.web_api.get_torrent_info(filename)
+ self.assertEquals(ret["name"], "azcvsupdater_2.6.2.jar")
+ self.assertEquals(ret["info_hash"], "ab570cdd5a17ea1b61e970bb72047de141bce173")
+ self.assertTrue("files_tree" in ret)
+
+ def test_get_magnet_info(self):
+ ret = self.deluge_web.web_api.get_magnet_info("magnet:?xt=urn:btih:SU5225URMTUEQLDXQWRB2EQWN6KLTYKN")
+ self.assertEquals(ret["name"], "953bad769164e8482c7785a21d12166f94b9e14d")
+ self.assertEquals(ret["info_hash"], "953bad769164e8482c7785a21d12166f94b9e14d")
+ self.assertTrue("files_tree" in ret)
+
+ @defer.inlineCallbacks
+ def test_get_torrent_files(self):
+ yield self.deluge_web.web_api.connect(self.host_id)
+ filename = os.path.join(os.path.dirname(__file__), "test.torrent")
+ torrents = [{"path": filename, "options": {"download_location": "/home/deluge/"}}]
+ self.deluge_web.web_api.add_torrents(torrents)
+ ret = yield self.deluge_web.web_api.get_torrent_files("ab570cdd5a17ea1b61e970bb72047de141bce173")
+ self.assertEquals(ret["type"], "dir")
+ self.assertEquals(ret["contents"], {u'azcvsupdater_2.6.2.jar':
+ {'priority': 1, u'index': 0, u'offset': 0, 'progress': 0.0, u'path':
+ u'azcvsupdater_2.6.2.jar', 'type': 'file', u'size': 307949}})
+
+ @defer.inlineCallbacks
+ def test_download_torrent_from_url(self):
+ filename = "ubuntu-9.04-desktop-i386.iso.torrent"
+ self.deluge_web.top_level.putChild(filename, File(common.rpath(filename)))
+ url = "http://localhost:%d/%s" % (self.webserver_listen_port, filename)
+ res = yield self.deluge_web.web_api.download_torrent_from_url(url)
+ self.assertTrue(res.endswith(filename))
+
+ @defer.inlineCallbacks
+ def test_invalid_json(self):
+ """
+ If json_api._send_response does not return server.NOT_DONE_YET
+ this error is thrown when json is invalid:
+ exceptions.RuntimeError: Request.write called on a request after Request.finish was called.
+
+ """
+ agent = Agent(reactor)
+ bad_body = '{ method": "auth.login" }'
+ d = yield agent.request(
+ 'POST',
+ 'http://127.0.0.1:%s/json' % self.webserver_listen_port,
+ Headers({'User-Agent': ['Twisted Web Client Example'],
+ 'Content-Type': ['application/json']}),
+ FileBodyProducer(StringIO(bad_body)))
+ yield d
diff --git a/deluge/ui/web/auth.py b/deluge/ui/web/auth.py
index d90273aea..ae6b11da5 100644
--- a/deluge/ui/web/auth.py
+++ b/deluge/ui/web/auth.py
@@ -16,7 +16,6 @@ from email.utils import formatdate
from twisted.internet.task import LoopingCall
-from deluge import component
from deluge.common import utf8_encoded
log = logging.getLogger(__name__)
@@ -79,9 +78,10 @@ class Auth(JSONComponent):
The component that implements authentification into the JSON interface.
"""
- def __init__(self):
+ def __init__(self, config):
super(Auth, self).__init__("Auth")
self.worker = LoopingCall(self._clean_sessions)
+ self.config = config
def start(self):
self.worker.start(5)
@@ -90,19 +90,18 @@ class Auth(JSONComponent):
self.worker.stop()
def _clean_sessions(self):
- config = component.get("DelugeWeb").config
- session_ids = config["sessions"].keys()
+ session_ids = self.config["sessions"].keys()
now = time.gmtime()
for session_id in session_ids:
- session = config["sessions"][session_id]
+ session = self.config["sessions"][session_id]
if "expires" not in session:
- del config["sessions"][session_id]
+ del self.config["sessions"][session_id]
continue
if time.gmtime(session["expires"]) < now:
- del config["sessions"][session_id]
+ del self.config["sessions"][session_id]
continue
def _create_session(self, request, login='admin'):
@@ -117,21 +116,18 @@ class Auth(JSONComponent):
m.update(os.urandom(32))
session_id = m.hexdigest()
- config = component.get("DelugeWeb").config
-
- expires, expires_str = make_expires(config["session_timeout"])
+ expires, expires_str = make_expires(self.config["session_timeout"])
checksum = str(make_checksum(session_id))
request.addCookie('_session_id', session_id + checksum,
path=request.base + "json", expires=expires_str)
log.debug("Creating session for %s", login)
- config = component.get("DelugeWeb").config
- if isinstance(config["sessions"], list):
- config.config["sessions"] = {}
+ if isinstance(self.config["sessions"], list):
+ self.config["sessions"] = {}
- config["sessions"][session_id] = {
+ self.config["sessions"][session_id] = {
"login": login,
"level": AUTH_LEVEL_ADMIN,
"expires": expires
@@ -139,7 +135,7 @@ class Auth(JSONComponent):
return True
def check_password(self, password):
- config = component.get("DelugeWeb").config
+ config = self.config
if "pwd_md5" in config.config:
# We are using the 1.2-dev auth method
log.debug("Received a password via the 1.2-dev auth method")
@@ -206,16 +202,15 @@ class Auth(JSONComponent):
:raises: Exception
"""
- config = component.get("DelugeWeb").config
session_id = get_session_id(request.getCookie("_session_id"))
- if session_id not in config["sessions"]:
+ if session_id not in self.config["sessions"]:
auth_level = AUTH_LEVEL_NONE
session_id = None
else:
- session = config["sessions"][session_id]
+ session = self.config["sessions"][session_id]
auth_level = session["level"]
- expires, expires_str = make_expires(config["session_timeout"])
+ expires, expires_str = make_expires(self.config["session_timeout"])
session["expires"] = expires
_session_id = request.getCookie("_session_id")
@@ -253,9 +248,8 @@ class Auth(JSONComponent):
salt = hashlib.sha1(os.urandom(32)).hexdigest()
s = hashlib.sha1(salt)
s.update(utf8_encoded(new_password))
- config = component.get("DelugeWeb").config
- config["pwd_salt"] = salt
- config["pwd_sha1"] = s.hexdigest()
+ self.config["pwd_salt"] = salt
+ self.config["pwd_sha1"] = s.hexdigest()
return True
@export
@@ -290,8 +284,7 @@ class Auth(JSONComponent):
:param session_id: the id for the session to remove
:type session_id: string
"""
- config = component.get("DelugeWeb").config
- del config["sessions"][__request__.session_id]
+ del self.config["sessions"][__request__.session_id]
return True
@export(AUTH_LEVEL_NONE)
diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py
index 7e63d7e81..af65af545 100644
--- a/deluge/ui/web/json_api.py
+++ b/deluge/ui/web/json_api.py
@@ -16,14 +16,12 @@ import shutil
import tempfile
import time
from types import FunctionType
-from urllib import unquote_plus
from twisted.internet import defer, reactor
from twisted.internet.defer import Deferred, DeferredList
from twisted.web import http, resource, server
-from deluge import component, httpdownloader
-from deluge.common import is_magnet
+from deluge import common, component, httpdownloader
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.ui import common as uicommon
from deluge.ui.client import Client, client
@@ -104,47 +102,6 @@ class JSON(resource.Resource, component.Component):
return methods
return client.daemon.get_method_list().addCallback(on_get_methods)
- def connect(self, host="localhost", port=58846, username="", password=""):
- """
- Connects the client to a daemon
- """
- d = client.connect(host, port, username, password)
-
- def on_client_connected(connection_id):
- """
- Handles the client successfully connecting to the daemon and
- invokes retrieving the method names.
- """
- d = self.get_remote_methods()
- component.get("Web.PluginManager").start()
- component.get("Web").start()
- return d
-
- return d.addCallback(on_client_connected)
-
- def disable(self):
- if not client.is_classicmode():
- client.disconnect()
- client.set_disconnect_callback(None)
-
- def enable(self):
- if not client.is_classicmode():
- client.set_disconnect_callback(self._on_client_disconnect)
- client.register_event_handler("PluginEnabledEvent", self.get_remote_methods)
- client.register_event_handler("PluginDisabledEvent", self.get_remote_methods)
- if component.get("DelugeWeb").config["default_daemon"]:
- # Sort out getting the default daemon here
- default = component.get("DelugeWeb").config["default_daemon"]
- host = component.get("Web").get_host(default)
- if host:
- self.connect(*host[1:])
- else:
- self.connect()
-
- def _on_client_disconnect(self, *args):
- component.get("Web.PluginManager").stop()
- component.get("Web").stop()
-
def _exec_local(self, method, params, request):
"""
Handles executing all local methods.
@@ -180,9 +137,8 @@ class JSON(resource.Resource, component.Component):
"""
try:
request.json = json.loads(request.json)
- except ValueError:
+ except (ValueError, TypeError):
raise JSONException("JSON not decodable")
-
if "method" not in request.json or "id" not in request.json or \
"params" not in request.json:
raise JSONException("Invalid JSON request")
@@ -257,12 +213,12 @@ class JSON(resource.Resource, component.Component):
request.setHeader("content-type", "application/x-json")
request.write(compress(response, request))
request.finish()
+ return server.NOT_DONE_YET
def render(self, request):
"""
Handles all the POST requests made to the /json controller.
"""
-
if request.method != "POST":
request.setResponseCode(http.NOT_ALLOWED)
request.finish()
@@ -421,7 +377,30 @@ class WebApi(JSONComponent):
except KeyError:
self.sessionproxy = SessionProxy()
- def get_host(self, host_id):
+ def disable(self):
+ if not client.is_classicmode():
+ client.disconnect()
+ client.set_disconnect_callback(None)
+
+ def enable(self):
+ if not client.is_classicmode():
+ client.set_disconnect_callback(self._on_client_disconnect)
+ client.register_event_handler("PluginEnabledEvent", self._json.get_remote_methods)
+ client.register_event_handler("PluginDisabledEvent", self._json.get_remote_methods)
+ if component.get("DelugeWeb").config["default_daemon"]:
+ # Sort out getting the default daemon here
+ default = component.get("DelugeWeb").config["default_daemon"]
+ host = component.get("Web")._get_host(default)
+ if host:
+ self._connect_daemon(*host[1:])
+ else:
+ self._connect_daemon()
+
+ def _on_client_disconnect(self, *args):
+ component.get("Web.PluginManager").stop()
+ self.stop()
+
+ def _get_host(self, host_id):
"""
Return the information about a host
@@ -442,6 +421,24 @@ class WebApi(JSONComponent):
self.core_config.stop()
self.sessionproxy.stop()
+ def _connect_daemon(self, host="localhost", port=58846, username="", password=""):
+ """
+ Connects the client to a daemon
+ """
+ d = client.connect(host, port, username, password)
+
+ def on_client_connected(connection_id):
+ """
+ Handles the client successfully connecting to the daemon and
+ invokes retrieving the method names.
+ """
+ d = self._json.get_remote_methods()
+ component.get("Web.PluginManager").start()
+ self.start()
+ return d
+
+ return d.addCallback(on_client_connected)
+
@export
def connect(self, host_id):
"""
@@ -452,16 +449,10 @@ class WebApi(JSONComponent):
:returns: the methods the daemon supports
:rtype: list
"""
- d = Deferred()
-
- def on_connected(methods):
- d.callback(methods)
- host = self.get_host(host_id)
+ host = self._get_host(host_id)
if host:
- self._json.connect(*host[1:]).addCallback(on_connected)
- else:
- return defer.fail(Exception("Bad host id"))
- return d
+ return self._connect_daemon(*host[1:])
+ return defer.fail(Exception("Bad host id"))
@export
def connected(self):
@@ -478,8 +469,12 @@ class WebApi(JSONComponent):
"""
Disconnect the web interface from the connected daemon.
"""
- client.disconnect()
- return True
+ d = client.disconnect()
+
+ def on_disconnect(reason):
+ return str(reason)
+ d.addCallback(on_disconnect)
+ return d
@export
def update_ui(self, keys, filter_dict):
@@ -677,47 +672,7 @@ class WebApi(JSONComponent):
@export
def get_magnet_info(self, uri):
- """
- Return information about a magnet link.
-
- :param uri: the magnet link
- :type uri: string
-
- :returns: information about the magnet link:
-
- ::
-
- {
- "name": the torrent name,
- "info_hash": the torrents info_hash,
- "files_tree": empty value for magnet links
- }
-
- :rtype: dictionary
- """
- magnet_scheme = 'magnet:?'
- xt_param = 'xt=urn:btih:'
- dn_param = 'dn='
- if uri.startswith(magnet_scheme):
- name = None
- info_hash = None
- for param in uri[len(magnet_scheme):].split('&'):
- if param.startswith(xt_param):
- xt_hash = param[len(xt_param):]
- if len(xt_hash) == 32:
- info_hash = base64.b32decode(xt_hash).encode("hex")
- elif len(xt_hash) == 40:
- info_hash = xt_hash
- else:
- break
- elif param.startswith(dn_param):
- name = unquote_plus(param[len(dn_param):])
-
- if info_hash:
- if not name:
- name = info_hash
- return {"name": name, "info_hash": info_hash, "files_tree": ''}
- return False
+ return common.get_magnet_info(uri)
@export
def add_torrents(self, torrents):
@@ -737,7 +692,7 @@ class WebApi(JSONComponent):
"""
for torrent in torrents:
- if is_magnet(torrent["path"]):
+ if common.is_magnet(torrent["path"]):
log.info("Adding torrent from magnet uri `%s` with options `%r`",
torrent["path"], torrent["options"])
client.core.add_torrent_magnet(torrent["path"], torrent["options"])
@@ -769,7 +724,7 @@ class WebApi(JSONComponent):
return host_id, host, port, status, info
try:
- host_id, host, port, user, password = self.get_host(host_id)
+ host_id, host, port, user, password = self._get_host(host_id)
except TypeError:
host = None
port = None
@@ -808,8 +763,8 @@ class WebApi(JSONComponent):
@export
def start_daemon(self, port):
"""
- Starts a local daemon.
- """
+ Starts a local daemon.
+ """
client.start_daemon(port, get_config_dir())
@export
@@ -821,7 +776,7 @@ class WebApi(JSONComponent):
:type host_id: string
"""
main_deferred = Deferred()
- host = self.get_host(host_id)
+ host = self._get_host(host_id)
if not host:
main_deferred.callback((False, _("Daemon doesn't exist")))
return main_deferred
@@ -864,7 +819,7 @@ class WebApi(JSONComponent):
# Check to see if there is already an entry for this host and return
# if thats the case
for entry in self.host_list["hosts"]:
- if (entry[0], entry[1], entry[2]) == (host, port, username):
+ if (entry[1], entry[2], entry[3]) == (host, port, username):
return (False, "Host already in the list")
try:
@@ -877,7 +832,7 @@ class WebApi(JSONComponent):
self.host_list["hosts"].append([connection_id, host, port, username,
password])
self.host_list.save()
- return (True,)
+ return True, connection_id
@export
def remove_host(self, connection_id):
@@ -887,7 +842,7 @@ class WebApi(JSONComponent):
:param host_id: the hash id of the host
:type host_id: string
"""
- host = self.get_host(connection_id)
+ host = self._get_host(connection_id)
if host is None:
return False
@@ -919,6 +874,9 @@ class WebApi(JSONComponent):
"""
web_config = component.get("DelugeWeb").config
for key in config.keys():
+ if key in ["sessions", "pwd_salt", "pwd_sha1"]:
+ log.warn("Ignored attempt to overwrite web config key '%s'" % key)
+ continue
if isinstance(config[key], basestring):
config[key] = config[key].encode("utf8")
web_config[key] = config[key]
diff --git a/deluge/ui/web/server.py b/deluge/ui/web/server.py
index d4d49f37e..31546401d 100644
--- a/deluge/ui/web/server.py
+++ b/deluge/ui/web/server.py
@@ -16,7 +16,7 @@ import tempfile
from OpenSSL.crypto import FILETYPE_PEM
from twisted.application import internet, service
-from twisted.internet import defer, error, reactor
+from twisted.internet import defer, reactor
from twisted.internet.ssl import SSL, Certificate, CertificateOptions, KeyPair
from twisted.web import http, resource, server, static
@@ -533,7 +533,6 @@ class DelugeWeb(component.Component):
def __init__(self):
super(DelugeWeb, self).__init__("DelugeWeb")
self.config = configmanager.ConfigManager("web.conf", CONFIG_DEFAULTS)
-
self.socket = None
self.top_level = TopLevel()
self.site = server.Site(self.top_level)
@@ -544,7 +543,7 @@ class DelugeWeb(component.Component):
self.cert = self.config["cert"]
self.base = self.config["base"]
self.web_api = WebApi()
- self.auth = Auth()
+ self.auth = Auth(self.config)
# Initalize the plugins
self.plugins = PluginManager()
@@ -568,17 +567,15 @@ class DelugeWeb(component.Component):
return 1
SetConsoleCtrlHandler(win_handler)
- def start(self, start_reactor=True):
+ def start(self):
log.info("%s %s.", _("Starting server in PID"), os.getpid())
if self.https:
self.start_ssl()
else:
self.start_normal()
- component.get("JSON").enable()
-
- if start_reactor:
- reactor.run()
+ component.get("Web").enable()
+ reactor.run()
def start_normal(self):
self.socket = reactor.listenTCP(self.port, self.site, interface=self.interface)
@@ -600,7 +597,7 @@ class DelugeWeb(component.Component):
def stop(self):
log.info("Shutting down webserver")
- component.get("JSON").disable()
+ component.get("Web").disable()
self.plugins.disable_plugins()
log.debug("Saving configuration file")
@@ -616,10 +613,8 @@ class DelugeWeb(component.Component):
def shutdown(self, *args):
self.stop()
- try:
- reactor.stop()
- except error.ReactorNotRunning:
- log.debug("Reactor not running")
+ reactor.stop()
+
if __name__ == "__builtin__":
deluge_web = DelugeWeb()
diff --git a/setup.cfg b/setup.cfg
index ae1a1b9fb..2eeb39b07 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,7 +16,7 @@ frameworks = CoreFoundation, Foundation, AppKit
[isort]
known_standard_library=unicodedata
-known_third_party=pygtk,gtk,gobject,gtk.gdk,pango,cairo,pangocairo,twisted,pytest,OpenSSL,pkg_resources,chardet,bbfreeze,win32verstamp
+known_third_party=pygtk,gtk,gobject,gtk.gdk,pango,cairo,pangocairo,twisted,pytest,OpenSSL,pkg_resources,chardet,bbfreeze,win32verstamp,mock
known_first_party=msgfmt
order_by_type=true
line_length=120
diff --git a/tox.ini b/tox.ini
index 427e713ab..38b3e7f0c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,6 +28,7 @@ deps =
pyopenssl
pyxdg
pytest
+ mock
whitelist_externals = py.test
commands = {envpython} setup.py test