From 4a781b21db10f82e35b9945109b5f4d41ad0e8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Thu, 15 Oct 2015 18:46:54 +0200 Subject: Server-side support for SAML logout, sessions are destroyed only using an AJAX call. --- web/main.js | 192 ++++++++++++++++++++++++++++++++++++------------------------ 1 file changed, 117 insertions(+), 75 deletions(-) (limited to 'web') 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; -- cgit v1.2.1