summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorAleš Smodiš <aless@guru.si>2015-10-15 18:46:54 +0200
committerAleš Smodiš <aless@guru.si>2015-10-15 18:46:54 +0200
commit4a781b21db10f82e35b9945109b5f4d41ad0e8c3 (patch)
tree3907cb657aeb9bf4ba27dcc630935329bac1a5b6 /web
parentde2ea4c96a007cd1c6545f0b4a063d3392a1a0d3 (diff)
Server-side support for SAML logout, sessions are destroyed only using an AJAX call.
Diffstat (limited to 'web')
-rw-r--r--web/main.js192
1 files changed, 117 insertions, 75 deletions
diff --git a/web/main.js b/web/main.js
index 67d4274..374f2fc 100644
--- a/web/main.js
+++ b/web/main.js
@@ -2,6 +2,7 @@ var engine = require('engine.io'), // web sockets communication handler, sitting
http = require('http'), // HTTP library
https = require('https'), // HTTPS library
http_server = http.createServer(), // the low-level HTTP handler
+ querystring = require('querystring'), // encodes and decodes URL query strings
net = require('net'), // TCP sockets library
Promise = require('bluebird'), // the promises library
log4js = require('log4js'), // the logger
@@ -9,10 +10,10 @@ var engine = require('engine.io'), // web sockets communication handler, sitting
express = require('express'), // library providing the Express web framework
http_app = express(); // web framework engine, sitting on the low-level HTTP handler
-var SAML_LOGINWAIT_URL = process.env.CODEQ_SAML_LOGINWAIT_URL || 'https://codeq.si/Shibboleth.sso/WaitLogin';
+var SAML_SERVICE_URL = process.env.CODEQ_SAML_SERVICE_URL || 'https://codeq.si/Shibboleth.sso/';
-var samlLoginwaitUrlParsed = url.parse(SAML_LOGINWAIT_URL),
- samlLoginwaitIsHttps = samlLoginwaitUrlParsed.protocol === 'https:';
+var samlUrlParsed = url.parse(SAML_SERVICE_URL),
+ samlIsHttps = samlUrlParsed.protocol === 'https:';
// ================================================================================
// The logger
@@ -85,7 +86,7 @@ http_app.get('/ws/logout', function (req, res) {
session = sessions[sid];
if (session) {
logger.debug('Logging out via AJAX, sid=' + sid);
- guiHandlers.logout(session, {'sid': sid, 'action': 'logout'});
+ guiHandlers.destroy(session, {'sid': sid, 'action': 'destroy'});
}
else if (sid) {
logger.warn('Cannot logout via AJAX: no session exists with sid=' + sid);
@@ -98,114 +99,141 @@ http_app.get('/ws/logout', function (req, res) {
});
-// ================================================================================
-// Web services stuff - handlers for GUI requests, usually just forwarded to Python
-// ================================================================================
-
-// connected clients
-var sessions = {
- // sid: sessions description {sid: string, lastActivity: Date.now(), socket: net.Socket}
-};
-
-// GUI action handlers, keyed by the action name, values are functions that take the session and the message, or truthy values
-var guiHandlers = {
- // special-case action handlers
- 'logout': function actionLogout(session, message) {
- // logout, the user quit the app
- logger.debug('Logout GUI');
- sendDataToPython(message).finally(function () {
- //session.end({'type': 'reset', 'code': 9999, 'message': 'Bye.'});
- session.end({'code': 0, 'message': 'Bye.', 'tid': message.tid, 'sid':message.sid});
- }).done();
- },
-
- 'saml_login': function samlLogin(session, message) {
- var guiResponse = {'tid': message.tid, 'sid': message.sid},
- aborted = false,
+var performSamlAction = function (samlAction, params) {
+ return new Promise(function (resolve, reject) {
+ var aborted = false,
options = {
- hostname: samlLoginwaitUrlParsed.hostname,
- port: samlLoginwaitUrlParsed.port || (samlLoginwaitIsHttps ? 443 : 80),
- path: samlLoginwaitUrlParsed.pathname + '?sid=' + session.sid,
+ hostname: samlUrlParsed.hostname,
+ port: samlUrlParsed.port || (samlIsHttps ? 443 : 80),
+ path: samlUrlParsed.pathname + samlAction + '?' + querystring.encode(params),
method: 'GET',
rejectUnauthorized: false // allow server certificates we don't know
},
handleResponse = function (res) {
var chunks = [],
- status = res.statusCode,
- headers, i, m, pythonRequest, keys;
- if (status !== 200) {
- guiResponse.code = -2;
- guiResponse.message = 'Received an error from the identity server: ' + status + ' ' + res.statusMessage;
- chunks.push(guiResponse.message, '\n');
- headers = res.rawHeaders;
- for (i = 0; i < headers.length; i++) chunks.push(headers[i], '\n');
- chunks.push('\n');
- }
+ headers, m, s;
res.on('data', function (d) {
chunks.push(d);
});
res.on('end', function () {
if (aborted) return; // already handled
- if (status !== 200) {
- logger.error(chunks.join(''));
- session.send(guiResponse);
+ if (res.statusCode !== 200) {
+ // log error with all headers
+ headers = res.rawHeaders;
+ if (headers instanceof Array) m = headers.join('\n') + '\n\n' + chunks.join('');
+ else m = chunks.join('');
+ s = 'Received an error from the identity server: ' + res.statusCode + ' ' + res.statusMessage;
+ logger.error(s + '\n' + m);
+ reject(new Error(s));
return;
}
try {
m = JSON.parse(chunks.join(''));
}
catch (e) {
- guiResponse.code = -3;
- guiResponse.message = 'Response from the identity server is not a JSON: ' + e;
- logException(guiResponse.message + '\n' + chunks.join(''), e);
- session.send(guiResponse);
+ logException('Response from the identity server is not a JSON: ' + e + '\n' + chunks.join(''), e);
+ reject(e);
return;
}
- if (m.code !== 0) {
- guiResponse.code = m.code || -5;
- guiResponse.message = m.message || 'Status message from the identity server is not set';
- session.send(guiResponse);
- }
- else {
- message.saml_data = m.data && m.data.userData; // add the data from SAML authentication, and forward everything to Python
- sendDataToPython(message)
- .then(function (jsonReply) {
- session.samlData = m.data; // perhaps we'll need the SAML data in the future
- session.send(jsonReply);
- }, function (exc) {
- guiResponse.code = -6;
- guiResponse.message = 'Python processing of SAML login generated an error: ' + exc;
- logException(guiResponse.message, exc);
- session.send(guiResponse);
- })
- .done();
- }
+ resolve(m);
});
},
req;
- if (samlLoginwaitIsHttps) req = https.request(options, handleResponse);
+ if (samlIsHttps) req = https.request(options, handleResponse);
else req = http.request(options, handleResponse);
req.on('error', function (e) {
if (aborted) return; // already handled
- guiResponse.code = -1;
- guiResponse.message = 'Error when trying to contact the identity server: ' + e;
logException('Error when trying to contact the identity server: ' + e, e);
- session.send(guiResponse);
+ reject(e instanceof Error ? e : new Error('' + e));
});
req.setTimeout(600000, function () { // 10 minutes timeout
+ var m = 'Timeout waiting for a reply from the identity server';
aborted = true;
req.abort();
- guiResponse.code = -4;
- guiResponse.message = 'Timeout waiting for a reply from the identity server';
- logger.error(guiResponse.message);
- session.send(guiResponse);
+ logger.error(m);
+ reject(new Error(m));
});
req.end();
+ });
+};
+
+// ================================================================================
+// Web services stuff - handlers for GUI requests, usually just forwarded to Python
+// ================================================================================
+
+// connected clients
+var sessions = {
+ // sid: sessions description {sid: string, lastActivity: Date.now(), socket: net.Socket}
+};
+
+// GUI action handlers, keyed by the action name, values are functions that take the session and the message, or truthy values
+var guiHandlers = {
+ // special-case action handlers
+ 'destroy': function actionLogout(session, message) {
+ // logout and destroy the session, the user quit the app
+ logger.debug('Destroy GUI');
+ sendDataToPython(message).finally(function () {
+ //session.end({'type': 'reset', 'code': 9999, 'message': 'Bye.'});
+ session.end({'code': 0, 'message': 'Bye.', 'tid': message.tid, 'sid':message.sid});
+ }).done();
+ },
+
+ 'saml_login': function samlLogin(session, message) {
+ var samlData = null;
+ performSamlAction('WaitLogin', {'sid': session.sid})
+ .then(function (jsonObj) {
+ if (jsonObj.code !== 0) return jsonObj;
+ samlData = jsonObj.data;
+ if (!samlData) return {'tid': message.tid, 'sid': message.sid, 'code': -2, 'message': 'IdP proxy did not return any SAML data'};
+ if (!samlData.userData) return {'tid': message.tid, 'sid': message.sid, 'code': -3, 'message': 'IdP proxy did not return any SAML user data'};
+ message.saml_data = samlData.userData; // add the data from SAML authentication, and forward everything to Python
+ return sendDataToPython(message);
+ })
+ .then(function (jsonObj) {
+ if (jsonObj.code !== 0) session.samlData = samlData; // we need the SAML data to be able to perform global logout (saml_logout action)
+ session.send(jsonObj);
+ })
+ .catch(function (e) {
+ var m = 'Error while trying to process the SAML request using the identity server: ' + e;
+ logException(m, e);
+ session.send({'tid': message.tid, 'sid': message.sid, 'code': -1, 'message': m});
+ })
+ .done();
+ },
+
+ 'saml_logout': function samlLogout(session, message) {
+ var samlData = session.samlData,
+ keys, sd, key, i;
+ if (!samlData) {
+ session.send({'tid': message.tid, 'sid': message.sid, 'code': -1, 'message': 'The session was not authenticated using SAML'});
+ return;
+ }
+ // make a copy of SAML data, excluding user data
+ keys = Object.keys(samlData);
+ sd = {};
+ for (i = keys.length - 1; i >= 0; i--) {
+ key = keys[i];
+ if (key !== 'userData') sd[key] = samlData[key];
+ }
+
+ performSamlAction('WaitLogout', {'sid': session.sid, 'saml': JSON.stringify(sd)})
+ .then(function (jsonObj) {
+ if (jsonObj.code !== 0) return jsonObj;
+ return sendDataToPython(message);
+ })
+ .then(session.send)
+ .catch(function (e) {
+ var m = 'Error while trying to process the SAML request using the identity server: ' + e;
+ logException(m, e);
+ session.send({'tid': message.tid, 'sid': message.sid, 'code': -2, 'message': m});
+ })
+ .done();
},
// actions to use default handling should define truthy values that are not functions
// (this is to filter out unnecessary traffic before it hits Python)
'login': true,
+ 'logout': true,
'signup': true,
'change_password': true,
'activity': true,
@@ -289,9 +317,23 @@ server.on('connection', function (socket) {
s.close();
s = null;
});
+ setTimeout(function () { // give it a second, then close it forcefully if it's still open
+ if (s) {
+ try {
+ s.close();
+ }
+ catch (e) {} // ignore
+ s = null;
+ }
+ }, 1000);
}
catch (e) {
logException('Failed to send reply to GUI about a fatal error: ' + e, e);
+ try {
+ s.close();
+ s = null;
+ }
+ catch (e) {} // ignore
}
socket = null; // this socket will no longer be processed
session.socket = null;