diff --git a/deluge/ui/web/auth.py b/deluge/ui/web/auth.py index 6422625c5..9a8aee94b 100644 --- a/deluge/ui/web/auth.py +++ b/deluge/ui/web/auth.py @@ -40,18 +40,47 @@ AUTH_LEVEL_ADMIN = 10 AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL +class AuthError(Exception): + """ + An exception that might be raised when checking a request for + authentication. + """ + pass + import time import random import hashlib import logging from twisted.internet.defer import Deferred +from twisted.internet.task import LoopingCall from deluge import component from deluge.ui.web.json_api import JSONComponent, export log = logging.getLogger(__name__) +def make_checksum(session_id): + return reduce(lambda x,y:x+y, map(ord, session_id)) + +def get_session_id(session_id): + """ + Checks a session id against its checksum + """ + if not session_id: + return None + + try: + checksum = int(session_id[-4:]) + session_id = session_id[:-4] + + if checksum == make_checksum(session_id): + return session_id + return None + except Exception, e: + log.exception(e) + return None + class Auth(JSONComponent): """ The component that implements authentification into the JSON interface. @@ -59,13 +88,31 @@ class Auth(JSONComponent): def __init__(self): super(Auth, self).__init__("Auth") + self.worker = LoopingCall(self._clean_sessions) + self.worker.start(5) - def _create_session(self, login='admin'): + def _clean_sessions(self): + config = component.get("DelugeWeb").config + session_ids = config["sessions"].keys() + + now = time.gmtime() + for session_id in session_ids: + session = config["sessions"][session_id] + + if "expires" not in session: + del config["sessions"][session_id] + continue + + if time.gmtime(session["expires"]) < now: + del config["sessions"][session_id] + continue + + def _create_session(self, request, login='admin'): """ Creates a new session. :keyword login: the username of the user logging in, currently \ - only for future use. + only for future use currently. :type login: string """ m = hashlib.md5() @@ -75,6 +122,16 @@ class Auth(JSONComponent): m.update(m.hexdigest()) session_id = m.hexdigest() + expires = int(time.time()) + 3600 + expires_str = time.strftime('%a, %d %b %Y %H:%M:%S UTC', + time.gmtime(expires)) + + checksum = str(make_checksum(session_id)) + + print 'Adding cookie' + request.addCookie('_session_id', session_id + checksum, + path="/json", expires=expires_str) + log.debug("Creating session for %s", login) config = component.get("DelugeWeb").config @@ -82,9 +139,54 @@ class Auth(JSONComponent): config.config["sessions"] = {} config["sessions"][session_id] = { - "login": login + "login": login, + "level": AUTH_LEVEL_ADMIN, + "expires": expires } - return session_id + return True + + def check_request(self, request, method=None, level=None): + """ + Check to ensure that a request is authorised to call the specified + method of authentication level. + + :param request: The HTTP request in question + :type request: twisted.web.http.Request + :keyword method: Check the specified method + :type method: function + :keyword level: Check the specified auth level + :type level: integer + + :raises: Exception + """ + + config = component.get("DelugeWeb").config + session_id = get_session_id(request.getCookie("_session_id")) + + if session_id not in config["sessions"]: + auth_level = AUTH_LEVEL_NONE + session_id = None + else: + auth_level = config["sessions"][session_id]["level"] + + if method: + if not hasattr(method, "_json_export"): + raise Exception("Not an exported method") + + method_level = getattr(method, "_json_auth_level") + if method_level is None: + raise Exception("Method has no auth level") + + level = method_level + + if level is None: + raise Exception("No level specified to check against") + + request.auth_level = auth_level + request.session_id = session_id + + if auth_level < level: + raise AuthError("Not authenticated") @export def change_password(self, new_password): @@ -104,25 +206,21 @@ class Auth(JSONComponent): config["pwd_sha1"] = s.hexdigest() d.callback(True) return d - - @export - def check_session(self, session_id): + @export(AUTH_LEVEL_NONE) + def check_session(self, session_id=None): """ Check a session to see if it's still valid. - :param session_id: the id for the session to remove - :type session_id: string :returns: True if the session is valid, False if not. :rtype: booleon """ d = Deferred() - config = component.get("DelugeWeb").config - d.callback(session_id in config["sessions"]) + d.callback(__request__.session_id is not None) return d @export - def delete_session(self, session_id): + def delete_session(self): """ Removes a session. @@ -131,11 +229,11 @@ class Auth(JSONComponent): """ d = Deferred() config = component.get("DelugeWeb").config - del config["sessions"][session_id] + del config["sessions"][__request__.session_id] d.callback(True) return d - @export + @export(AUTH_LEVEL_NONE) def login(self, password): """ Test a password to see if it's valid. @@ -177,7 +275,7 @@ class Auth(JSONComponent): m.update(password) if m.digest() == decodestring(config["old_pwd_md5"]): # We have a match, so we can create and return a session id. - d.callback(self._create_session()) + d.callback(self._create_session(__request__)) # We also want to move the password over to sha1 and remove # the old passwords from the config file. @@ -193,7 +291,7 @@ class Auth(JSONComponent): s.update(password) if s.hexdigest() == config["pwd_sha1"]: # We have a match, so we can create and return a session id. - d.callback(self._create_session()) + d.callback(self._create_session(__request__)) else: # Can't detect which method we should be using so just deny diff --git a/deluge/ui/web/js/Deluge.Client.js b/deluge/ui/web/js/Deluge.Client.js index bea208e21..a2152838d 100644 --- a/deluge/ui/web/js/Deluge.Client.js +++ b/deluge/ui/web/js/Deluge.Client.js @@ -103,28 +103,34 @@ Ext.namespace('Ext.ux.util'); errorObj = { id: options.id, result: null, - error: 'HTTP: ' + response.status + ' ' + response.statusText - } + error: { + msg: 'HTTP: ' + response.status + ' ' + response.statusText, + code: 255 + } + } + + this.fireEvent('error', errorObj, response, requestOptions) + if (Ext.type(options.failure) != 'function') return; if (options.scope) { options.failure.call(options.scope, errorObj, response, requestOptions); } else { options.failure(errorObj, response, requestOptions); - } - this.fireEvent('error', errorObj, response, requestOptions) + } }, _onSuccess: function(response, requestOptions) { var responseObj = Ext.decode(response.responseText); var options = requestOptions.options; if (responseObj.error) { + this.fireEvent('error', responseObj, response, requestOptions); + if (Ext.type(options.failure) != 'function') return; if (options.scope) { options.failure.call(options.scope, responseObj, response, requestOptions); } else { options.failure(responseObj, response, requestOptions); } - this.fireEvent('error', responseObj, response, requestOptions) } else { if (Ext.type(options.success) != 'function') return; if (options.scope) { diff --git a/deluge/ui/web/js/Deluge.Login.js b/deluge/ui/web/js/Deluge.Login.js index eeec478a9..17251f28e 100644 --- a/deluge/ui/web/js/Deluge.Login.js +++ b/deluge/ui/web/js/Deluge.Login.js @@ -57,9 +57,7 @@ Copyright: initComponent: function() { Ext.deluge.LoginWindow.superclass.initComponent.call(this); - Deluge.Events.on('logout', this.onLogout, this); this.on('show', this.onShow, this); - this.on('beforeshow', this.onBeforeShow, this); this.addButton({ text: _('Login'), @@ -89,6 +87,33 @@ Copyright: }) }, + show: function(skipCheck) { + if (this.firstShow) { + Deluge.Client.on('error', this.onClientError, this); + this.firstShow = false; + } + + if (skipCheck) { + return Ext.deluge.LoginWindow.superclass.show.call(this); + } + + Deluge.Client.auth.check_session({ + success: function(result) { + if (result) { + Deluge.Events.fire('login'); + this.loginForm.items.get('password').setRawValue(''); + this.hide(); + } else { + this.show(true); + } + }, + failure: function(result) { + this.show(true); + }, + scope: this + }); + }, + onKey: function(field, e) { if (e.getKey() == 13) this.onLogin(); }, @@ -101,7 +126,6 @@ Copyright: Deluge.Events.fire('login'); this.hide(); passwordField.setRawValue(''); - Deluge.UI.cookies.set("session", result); } else { Ext.MessageBox.show({ title: _('Login Failed'), @@ -121,39 +145,19 @@ Copyright: }, onLogout: function() { - var session = Deluge.UI.cookies.get("session", false); - if (session) { - Deluge.Client.auth.delete_session(session, { - success: function(result) { - Deluge.UI.cookies.clear("session"); - this.show(); - }, - scope: this - }); - } + Deluge.Events.fire('logout'); + Deluge.Client.auth.delete_session({ + success: function(result) { + this.show(true); + }, + scope: this + }); }, - onBeforeShow: function() { - var session = Deluge.UI.cookies.get("session", false); - if (session) { - Deluge.Client.auth.check_session(session, { - success: function(result) { - if (result) { - Deluge.Events.fire('login'); - this.loginForm.items.get('password').setRawValue(''); - this.hide(); - } else { - Deluge.UI.cookies.clear("session"); - this.show(); - } - }, - failure: function(result) { - Deluge.UI.cookies.clear("session"); - this.show(); - }, - scope: this - }); - return false; + onClientError: function(errorObj, response, requestOptions) { + if (errorObj.error.code == 1) { + Deluge.Events.fire('logout'); + this.show(true); } }, diff --git a/deluge/ui/web/js/Deluge.Toolbar.js b/deluge/ui/web/js/Deluge.Toolbar.js index b39dccd40..eb68eedc8 100644 --- a/deluge/ui/web/js/Deluge.Toolbar.js +++ b/deluge/ui/web/js/Deluge.Toolbar.js @@ -150,8 +150,7 @@ Copyright: onLogout: function() { this.items.get('logout').disable(); - Deluge.Events.fire('logout'); - Deluge.Login.show(); + Deluge.Login.logout(); }, onConnectionManagerClick: function() { diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py index af47e9de1..152a28ab1 100644 --- a/deluge/ui/web/json_api.py +++ b/deluge/ui/web/json_api.py @@ -56,6 +56,7 @@ json = common.json log = logging.getLogger(__name__) AUTH_LEVEL_DEFAULT = None +AuthError = None class JSONComponent(component.Component): def __init__(self, name, interval=1, depend=None): @@ -74,9 +75,9 @@ def export(auth_level=AUTH_LEVEL_DEFAULT): :type auth_level: int """ - global AUTH_LEVEL_DEFAULT + global AUTH_LEVEL_DEFAULT, AuthError if AUTH_LEVEL_DEFAULT is None: - from deluge.ui.web.auth import AUTH_LEVEL_DEFAULT + from deluge.ui.web.auth import AUTH_LEVEL_DEFAULT, AuthError def wrap(func, *args, **kwargs): func._json_export = True @@ -153,6 +154,7 @@ class JSON(resource.Resource, component.Component): # and any plugins. meth = self._local_methods[method] meth.func_globals['__request__'] = request + component.get("Auth").check_request(request, meth) return meth(*params) raise JSONException("Unknown system method") @@ -160,6 +162,7 @@ class JSON(resource.Resource, component.Component): """ Executes methods using the Deluge client. """ + component.get("Auth").check_request(request, level=AUTH_LEVEL_DEFAULT) component, method = method.split(".") return getattr(getattr(client, component), method)(*params) @@ -169,7 +172,6 @@ class JSON(resource.Resource, component.Component): the rpc object that should be contained, returning a deferred for all procedure calls and the request id. """ - request_id = None try: request.json = json.loads(request.json) except ValueError: @@ -181,20 +183,25 @@ class JSON(resource.Resource, component.Component): method, params = request.json["method"], request.json["params"] request_id = request.json["id"] + result = None + error = None try: - if method.startswith("system."): - return self._exec_local(method, params, request), request_id - elif method in self._local_methods: - return self._exec_local(method, params, request), request_id + if method.startswith("system.") or method in self._local_methods: + result = self._exec_local(method, params, request) elif method in self._remote_methods: - return self._exec_remote(method, params), request_id + result = self._exec_remote(method, params) + else: + error = {"message": "Unknown method", "code": 2} + except AuthError, e: + error = {"message": "Not authenticated", "code": 1} except Exception, e: log.error("Error calling method `%s`", method) log.exception(e) - d = Deferred() - d.callback(None) - return d, request_id + + error = {"message": e.message, "code": 3} + + return request_id, result, error def _on_rpc_request_finished(self, result, response, request): """ @@ -207,7 +214,6 @@ class JSON(resource.Resource, component.Component): """ Handles any failures that occured while making an rpc call. """ - print type(reason) request.setResponseCode(http.INTERNAL_SERVER_ERROR) return "" @@ -218,10 +224,14 @@ class JSON(resource.Resource, component.Component): """ log.debug("json-request: %s", request.json) response = {"result": None, "error": None, "id": None} - d, response["id"] = self._handle_request(request) - d.addCallback(self._on_rpc_request_finished, response, request) - d.addErrback(self._on_rpc_request_failed, response, request) - return d + response["id"], d, response["error"] = self._handle_request(request) + + if isinstance(d, Deferred): + d.addCallback(self._on_rpc_request_finished, response, request) + d.addErrback(self._on_rpc_request_failed, response, request) + return d + else: + return self._send_response(request, response) def _on_json_request_failed(self, reason, request): """