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( @@ -137,11 +152,19 @@ class ClientTestCase(BaseTestCase, DaemonBase): d.addCallbacks(self.fail, on_failure) 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 +# +# 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):" + " : 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 +# +# 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