complete the new auth system using cookies & sessions
This commit is contained in:
parent
1b17d66576
commit
3e8c17b071
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -150,8 +150,7 @@ Copyright:
|
|||
|
||||
onLogout: function() {
|
||||
this.items.get('logout').disable();
|
||||
Deluge.Events.fire('logout');
|
||||
Deluge.Login.show();
|
||||
Deluge.Login.logout();
|
||||
},
|
||||
|
||||
onConnectionManagerClick: function() {
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue