/* CodeQ: an online programming tutor.
Copyright (C) 2015 UL FRI
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
var fs = require('fs'),
url = require('url'),
zlib = require('zlib'),
querystring = require('querystring'),
Promise = require('bluebird'),
log4js = require('log4js'), // the logger
express = require('express'),
xmlCrypto = require('xml-crypto'),
xmldom = require('xmldom'),
xmlbuilder = require('xmlbuilder'),
crypto = require('crypto'),
bodyParser = require('body-parser'),
http_app = express(),
http_server = require('http').createServer(http_app);
http_app.use(bodyParser.urlencoded({extended: true}));
http_app.use(bodyParser.json());
http_server.listen(8085, 'localhost');
// ================================================================================
// The logger
// ================================================================================
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file(process.env.CODEQ_SAML_LOG || 'codeq-saml.log'), 'log');
var logger = log4js.getLogger('log');
var logException = function (msg, error) {
if ((error instanceof Error) && (typeof error.stack === 'string')) msg += '\n' + error.stack;
logger.error(msg);
};
// prevent the node from exiting because of an unhandled exception
Promise.onPossiblyUnhandledRejection(function (error) {
logException('Unhandled promise rejection: ' + error, error);
});
process.on('uncaughtException', function (error) {
logException('Uncaught exception: ' + error, error);
});
// ================================================================================
// SP and IdP definitions
// ================================================================================
// SP (that's us)
//---------------
// our URL where the IdP responds with a login result
var assertionConsumerServiceURL = 'https://codeq.si/saml/assert', // must be a POST
// our issuer ID
SPIssuer = 'https://codeq.si/sp/201509281041',
// our encryption key for signing requests
privateCert = fs.readFileSync('/etc/shibboleth/sp-key.pem', 'utf8'),
// the status value that signals a success of an operation (a SAML constant)
SamlStatusSuccess = 'urn:oasis:names:tc:SAML:2.0:status:Success';
/*
// IdP: idp.aai.arnes.si
//----------------------
// the IdP's single-sign-on page URL, where a user is redirected to perform his/her login
var SSOServiceURL = 'https://idp.aai.arnes.si/simplesaml/saml2/idp/SSOService.php', // must be a GET
// the IdP's single-logout page URL, where a user is redirected to perform his/her (global) logout
SLOServiceURL = 'https://idp.aai.arnes.si/simplesaml/saml2/idp/SingleLogoutService.php', // must be a GET
// the IdP's issuer ID
IdPIssuer = 'https://idp.aai.arnes.si/idp/20090116',
// the IdP's public certificate to verify signatures with
IdPKeyInfoProvider = new xmlCrypto.FileKeyInfo('/etc/shibboleth/idp.crt');
*/
// IdP: idp.uni-lj.si
//--------------------
/*var SSOServiceURL = 'https://idp.uni-lj.si/simplesaml/saml2/idp/SSOService.php',
SLOServiceURL = 'https://idp.uni-lj.si/simplesaml/saml2/idp/SingleLogoutService.php',*/
var SSOServiceURL = 'https://codeq.si/idp/simplesaml/saml2/idp/SSOService.php',
SLOServiceURL = 'https://codeq.si/idp/simplesaml/saml2/idp/SingleLogoutService.php',
IdPIssuer = 'https://idp.uni-lj.si/idp/20100525',
IdPKeyInfoProvider = new xmlCrypto.FileKeyInfo('/etc/shibboleth/idp.uni-lj.si.crt');
// ================================================================================
// Misc
// ================================================================================
var idChars = '0123456789abcdef',
makeRequestId = function () {
var idParts = [],
i;
for (i = 0; i < 20; i++) idParts.push(idChars[Math.floor(Math.random() * 15)]);
return idParts.join('');
};
var waitingLogins = {}, // maps sid to waiter
waitingLogouts = {},
parkedLoginRequests = {}, // maps SAML request ID to waiter
parkedLogoutRequests = {};
var makeSamlRedirectUrl = function (serviceUrl, queryKey, queryObj, additionalData, callback) {
var xml = null;
try {
xml = xmlbuilder.create(queryObj).end();
logger.debug('Redirecting with ' + queryKey + ' to ' + serviceUrl + ':\n' + xml);
zlib.deflateRaw(xml, function (err, buffer) {
var svcUrl, signer, query, keys, i, key;
if (err) {
callback(err, null, xml);
return;
}
query = {
'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
};
query[queryKey] = buffer.toString('base64');
if (additionalData && (typeof additionalData === 'object')) {
keys = Object.keys(additionalData);
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
query[key] = additionalData[key];
}
}
signer = crypto.createSign('RSA-SHA1');
signer.update(querystring.stringify(query));
query.Signature = signer.sign(privateCert, 'base64');
svcUrl = url.parse(serviceUrl);
svcUrl.query = query;
delete svcUrl.search;
callback(null, url.format(svcUrl), xml);
});
}
catch (e) {
callback(e, null, xml);
}
};
var makeWaiter = function (sid) {
var waiter = {
'sid': sid,
'created': Date.now(),
'listeners': 0
};
waiter.promise = new Promise(function (resolve, reject) {
waiter.resolve = resolve;
waiter.reject = reject;
});
return waiter;
};
var makeDoubleWaiter = function (sid) {
var waiter = makeWaiter(sid);
waiter.responsePromise = new Promise(function (resolve, reject) {
waiter.responseResolve = resolve;
waiter.responseReject = reject;
});
return waiter;
};
// ================================================================================
// XML DOM data extraction functions for SAML data
// ================================================================================
var validateXmlSignature = function (xml, signatureNode) {
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = IdPKeyInfoProvider;
sig.loadSignature(signatureNode.toString());
return sig.checkSignature(xml);
};
var xmlStatusOk = function (statusNode) {
var node = statusNode.firstChild,
attributes, i;
while (node) {
if (node.localName === 'StatusCode') {
attributes = node.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'Value') return attributes[i].value == SamlStatusSuccess;
}
}
node = node.nextSibling;
}
return null;
};
var parseAttributeValue = function (attributeNode) {
var node = attributeNode.firstChild;
while (node) {
if (node.localName === 'AttributeValue') {
return node.firstChild.data;
}
node = node.nextSibling;
}
return null;
};
var parseAttributes = function (attributeStatementNode) {
var node = attributeStatementNode.firstChild,
result = {},
attributes, i, name;
while (node) {
if (node.localName === 'Attribute') {
attributes = node.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'Name') {
name = attributes[i].value;
break;
}
}
if (i < 0) {
logger.error('ERROR: an attribute without a name');
}
else {
result[name] = parseAttributeValue(node);
}
}
node = node.nextSibling;
}
return result;
};
// return the NameID in the format for use with xmlbuilder
var parseNameID = function (NameIDNode) {
var result = {},
i, attributes;
result['#text'] = NameIDNode.firstChild.data;
attributes = NameIDNode.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
switch (attributes[i].localName) {
case 'NameQualifier': // NameID namespace
result['@NameQualifier'] = attributes[i].value;
break;
case 'SPNameQualifier': // 2nd level NameID namespace
result['@SPNameQualifier'] = attributes[i].value;
break;
case 'Format':
result['@Format'] = attributes[i].value;
break;
case 'SPProvidedID':
result['@SPProvidedID'] = attributes[i].value;
break;
}
}
return result;
};
var parseSubject = function (subjectNode) {
var node = subjectNode.firstChild,
result = {};
while (node) {
if (node.localName === 'NameID') {
result['NameID'] = parseNameID(node);
}
node = node.nextSibling;
}
return result;
};
var parseAssertion = function (xml, assertionNode) {
var node = assertionNode.firstChild,
result = {},
attributes, i;
while (node) {
switch (node.localName) {
case 'Signature':
result.signatureOk = validateXmlSignature(xml, node);
break;
case 'Subject':
// extract presenter identifier
result.subject = parseSubject(node);
break;
case 'AuthnStatement':
// extract the session index
attributes = node.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'SessionIndex') {
result.SessionIndex = attributes[i].value;
break;
}
}
break;
case 'AttributeStatement':
result.attributes = parseAttributes(node);
break;
}
node = node.nextSibling;
}
return result;
};
// ================================================================================
// SAML logout handlers
// ================================================================================
// process response to our (global) logout request
var handleLogoutResponse = function (xml, form, res) {
var errors = [],
issuer = null,
status = null,
waiter = null,
responded = false, // don't respond if we already responded (and failed)
doc, rootNode, attributes, i, inResponseTo, node, m;
try {
doc = new xmldom.DOMParser().parseFromString(xml);
rootNode = doc.documentElement;
if (rootNode.localName != 'LogoutResponse') {
errors.push('not a LogoutResponse: ' + rootNode.localName);
}
else {
attributes = rootNode.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'InResponseTo') {
inResponseTo = attributes[i].value;
break;
}
}
if (!inResponseTo) errors.push('request ID that this message is a response to is missing');
else {
waiter = parkedLogoutRequests[inResponseTo];
if (!waiter) errors.push('there is no session waiting on request ID ' + inResponseTo);
else delete parkedLogoutRequests[inResponseTo];
}
node = rootNode.firstChild;
while (node) {
switch (node.localName) {
case 'Issuer':
issuer = node.firstChild.data;
break;
case 'Status':
status = xmlStatusOk(node);
break;
default:
// errors.push('Unknown/unsupported top XML node: ' + node.localName + ', prefix: ' + node.prefix);
break;
}
node = node.nextSibling;
}
if (issuer === null) errors.push('Issuer is not set');
else if (issuer !== IdPIssuer) errors.push('Invalid IdP issuer: ' + issuer);
if (status === null) errors.push('response status is not set');
else if (!status) errors.push('response status is not OK');
}
if (errors.length > 0) {
if (waiter) {
waiter.done = Date.now();
waiter.responseReject(new Error(errors.join(', ')));
}
if (errors.length > 1) m = 'Multiple errors:\n ' + errors.join('\n ');
else m = 'Error: ' + errors[0];
logger.error('Received a SAML logout response and encountered errors processing it; response:\n' + xml + '\nErrors:\n ' + errors.join('\n '));
}
else {
if (waiter) {
waiter.done = Date.now();
waiter.responseResolve();
}
m = 'OK';
logger.debug('Received a SAML logout response:\n' + xml);
}
responded = true;
res.set('Content-Type', 'text/plain');
res.send(m);
}
catch (e) {
m = 'Error while processing a logout response: ' + e;
logException(m + '\n' + xml, e);
if (waiter) {
waiter.done = Date.now();
waiter.responseReject(e);
}
if (!responded) {
res.set('Content-Type', 'text/plain');
res.send(m);
}
}
};
// a third party initiated the (global) logout, process the request and redirect with a response
var handleLogoutRequest = function (xml, form, res) {
var errors = [],
nameId = null,
sessionIndex = null,
responded = false, // don't respond if we already responded (and failed)
doc, rootNode, attributes, i, requestId, node,
responseObj, responseId, m;
try {
doc = new xmldom.DOMParser().parseFromString(xml);
rootNode = doc.documentElement;
if (rootNode.localName != 'LogoutRequest') {
errors.push('not a LogoutRequest: ' + rootNode.localName);
}
else {
attributes = rootNode.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'ID') {
requestId = attributes[i].value;
break;
}
}
if (!requestId) errors.push('request ID is missing');
node = rootNode.firstChild;
while (node) {
switch (node.localName) {
case 'Issuer':
if (node.firstChild.data !== IdPIssuer) errors.push('ERROR: invalid IdP issuer: ' + node.firstChild.data);
break;
case 'NameID':
nameId = parseNameID(node);
break;
case 'SessionIndex':
sessionIndex = node.firstChild && node.firstChild.data;
break;
}
node = node.nextSibling;
}
if (!nameId) errors.push('NameID not present');
else {
if (!nameId['#text']) errors.push('NameID is empty');
}
}
if (errors.length > 0) {
logger.error('A third party requested a global SAML logout, but there were errors; original request:\n' + xml + '\nErrors:\n' + errors.join('\n'));
responded = true;
res.set('Content-Type', 'text/plain');
res.send(errors.join('\n'));
}
else {
responseId = makeRequestId();
responseObj = {
'samlp:LogoutResponse': {
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'@Version': '2.0',
'@Destination': SLOServiceURL,
'@ID': responseId,
'@IssueInstant': new Date().toISOString(),
'@InResponseTo': requestId,
'saml:Issuer': {
'#text': SPIssuer
},
'samlp:Status': {
'samlp:StatusCode': {
'@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success'
}
}
}
};
makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', responseObj, form.RelayState ? {RelayState: form.RelayState} : null, function (err, svcUrl, responseXml) {
if (err) {
logException('Error while assembling the SAML logout response; original request:\n' + xml + '\nError: ' + err, err);
responded = true;
res.set('Content-Type', 'text/plain');
res.send('Failed: ' + err);
return;
}
logger.debug('Responding to a third party logout request:\n' + xml + '\nResponse:\n' + responseXml);
responded = true;
res.redirect(svcUrl); // redirect to the IdP
});
}
}
catch (e) {
m = 'Error while processing a third party logout request: ' + e;
logException(m + '\n' + xml, e);
if (!responded) {
res.set('Content-Type', 'text/plain');
res.send(m);
}
}
};
// ================================================================================
// SAML web request handlers
// ================================================================================
// redirect to login at IdP, https://codeq.si/saml/Login
http_app.get('/saml/Login', function (req, res) {
var sid = req.query.sid,
responded = false, // don't respond if we already responded (and failed)
waiter, requestId, request;
try {
if (!sid) {
logger.error('Login request without SID, query parameters: ' + JSON.stringify(req.query));
responded = true;
res.set('Content-Type', 'text/plain');
res.send('SID not set');
return;
}
waiter = waitingLogins[sid];
if (!waiter) waitingLogins[sid] = waiter = makeWaiter(sid);
requestId = makeRequestId();
request = {
'samlp:AuthnRequest': {
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'@Version': '2.0',
'@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
'@AssertionConsumerServiceURL': assertionConsumerServiceURL,
'@Destination': SSOServiceURL,
'@ForceAuthn': 'true',
'@ID': requestId,
'@IssueInstant': new Date().toISOString(),
'saml:Issuer': {
'#text': SPIssuer
}
}
};
makeSamlRedirectUrl(SSOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl, requestXml) {
if (err) {
logException('Error while assembling the Login SAML request for sid=' + sid + ': ' + err, err);
waiter.done = Date.now();
waiter.reject(err instanceof Error ? err : new Error(err));
responded = true;
res.set('Content-Type', 'text/plain');
res.send('Failed: ' + err);
return;
}
logger.debug('Redirecting to IdP with a login request:\n' + requestXml);
responded = true;
res.redirect(svcUrl); // redirect to the IdP
parkedLoginRequests[requestId] = waiter;
waiter.parked = Date.now();
waiter.requestId = requestId;
});
}
catch (e) {
logException('Exception while processing login request with query parameters ' + JSON.stringify(req.query) + ': ' + e, e);
if (waiter) {
waiter.done = Date.now();
waiter.reject(e);
}
if (!responded) {
res.set('Content-Type', 'text/plain');
res.send('Error: ' + e);
}
}
});
// redirect to logout at IdP, https://codeq.si/saml/Logout
http_app.get('/saml/Logout', function (req, res) {
var sid = req.query.sid,
responded = false,
waiter, requestId, request;
try {
if (!sid) {
logger.error('Logout request without SID, query parameters: ' + JSON.stringify(req.query));
responded = true;
res.set('Content-Type', 'text/plain');
res.send('SID not set');
return;
}
waiter = waitingLogouts[sid];
if (!waiter) waitingLogouts[sid] = waiter = makeDoubleWaiter(sid);
waiter.promise
.then(function (samlData) {
requestId = makeRequestId();
request = {
'samlp:LogoutRequest': {
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
'@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
'@Version': '2.0',
'@Destination': SLOServiceURL,
'@ID': requestId,
'@IssueInstant': new Date().toISOString(),
'saml:Issuer': {
'#text': SPIssuer
},
'saml:NameID': samlData.NameID
}
};
if (samlData.SessionIndex) request['samlp:LogoutRequest']['samlp:SessionIndex'] = {'#text': samlData.SessionIndex};
makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl, requestXml) {
if (err) {
logException('Error while assembling the Logout SAML request for sid=' + sid + ': ' + err, err);
waiter.done = Date.now();
waiter.responseReject(err instanceof Error ? err : new Error(err));
responded = true;
res.set('Content-Type', 'text/plain');
res.send('Failed: ' + err);
return;
}
logger.debug('Redirecting to IdP with a logout request:\n' + requestXml);
responded = true;
res.redirect(svcUrl); // redirect to the IdP
parkedLogoutRequests[requestId] = waiter;
waiter.parked = Date.now();
waiter.requestId = requestId;
});
})
.catch(function (e) {
logException('Exception while processing logout request: ' + e, e);
if (waiter) {
if (requestId) delete parkedLogoutRequests[requestId];
waiter.done = Date.now();
waiter.responseReject(e);
}
responded = true;
res.set('Content-Type', 'text/plain');
res.send('Error: ' + e);
})
.done();
}
catch (e) {
logException('Exception while processing logout request: ' + e, e);
if (waiter) {
waiter.done = Date.now();
waiter.reject(e);
}
if (!responded) {
res.set('Content-Type', 'text/plain');
res.send('Error: ' + e);
}
}
});
// user logged in, read the credentials
http_app.post('/saml/assert', function (req, res) {
var form = req.body,
keys = [],
errors = [],
inResponseTo = null,
issuer = null,
signature = null,
status = null,
assertion = null,
waiter = null,
responded = false, // don't respond if we already responded (and failed)
attributes, i, key, value, xml, doc, rootNode, node, NameID, SessionIndex, samlData, userData, m;
try {
value = form.SAMLResponse;
if (!value) {
errors.push('no SAMLResponse');
}
else {
xml = new Buffer(value, 'base64').toString('utf8');
doc = new xmldom.DOMParser().parseFromString(xml);
rootNode = doc.documentElement;
if (rootNode.localName != 'Response') {
errors.push('the root XML node is not a Response: ' + rootNode.localName);
}
else {
attributes = rootNode.attributes;
for (i = attributes.length - 1; i >= 0; i--) {
if (attributes[i].localName === 'InResponseTo') {
inResponseTo = attributes[i].value;
break;
}
}
if (!inResponseTo) errors.push('request ID that this message is a response to is missing');
else {
waiter = parkedLoginRequests[inResponseTo];
if (!waiter) errors.push('there is no session waiting on request ID ' + inResponseTo);
else {
waiter.done = Date.now();
delete parkedLoginRequests[inResponseTo];
}
node = rootNode.firstChild;
while (node) {
switch (node.localName) {
case 'Issuer':
issuer = node.firstChild.data;
break;
case 'Signature':
signature = validateXmlSignature(xml, node);
break;
case 'Status':
status = xmlStatusOk(node);
break;
case 'Assertion':
assertion = parseAssertion(xml, node);
break;
default:
// errors.push('Unknown/unsupported top XML node: ' + node.localName + ', prefix: ' + node.prefix + '
\n');
break;
}
node = node.nextSibling;
}
if (issuer === null) errors.push('Issuer is not set');
else if (issuer !== IdPIssuer) errors.push('Invalid IdP issuer: ' + issuer);
if (signature === null) errors.push('the response is not signed');
else if (!signature) errors.push('IdP signature is invalid');
if (status === null) errors.push('status is not set');
else if (!status) errors.push('response status is not OK');
if (!assertion) errors.push('no assertions supplied, no attributes present');
else {
if ((typeof assertion.signatureOk !== 'undefined') && (!assertion.signatureOk)) errors.push('assertion signature is invalid');
samlData = {};
node = assertion.subject;
if (!node) errors.push('assertion subject is not set');
else {
node = node.NameID;
if (!node) errors.push('assertion subject\'s NameID is not set');
else {
if (!node['#text']) errors.push('assertion subject\'s NameID is empty');
samlData['NameID'] = node;
}
}
if (assertion.SessionIndex) samlData['SessionIndex'] = assertion.SessionIndex;
node = assertion.attributes;
if (!node) errors.push('no attributes supplied');
else {
userData = {};
samlData.userData = userData;
keys = Object.keys(node);
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
userData[key] = node[key];
}
}
}
}
}
}
if (errors.length > 0) {
logger.debug('Errors processing a login response:\nSAMLResponse=' + value + '\nDecoded XML:\n' + xml + '\nErrors:\n' + errors.join('\n'));
if (waiter) waiter.reject(new Error(errors.join(', ')));
if (errors.length > 1) m = 'Multiple errors:\n ' + errors.join('\n ');
else m = 'Error: ' + errors[0];
}
else {
logger.debug('Received a successful login response:\n' + xml);
if (waiter) waiter.resolve(samlData);
m = 'OK';
}
responded = true;
res.set('Content-Type', 'text/plain');
res.send(m);
}
catch (e) {
m = 'Error while processing a login response: ' + e;
if (xml) logException(m + '\n' + xml, e);
else if (value) logException(m + '\nSAMLResponse=' + value, e);
else logException(m, e);
if (!responded) {
res.set('Content-Type', 'text/plain');
res.send(m);
}
}
});
// user logged out, the GET service
http_app.get('/saml/slo', function (req, res) {
var form = req.query,
delegateToHandler = function (err, buffer, handler) {
if (err) {
logException('Error while deflating request: ' + err, err);
res.send('Failed: ' + err);
return;
}
handler(buffer.toString('utf8'), form, res);
};
try {
if (form.SAMLRequest) zlib.inflateRaw(new Buffer(form.SAMLRequest, 'base64'), function (err, buffer) {
delegateToHandler(err, buffer, handleLogoutRequest);
});
else if (form.SAMLResponse) zlib.inflateRaw(new Buffer(form.SAMLResponse, 'base64'), function (err, buffer) {
delegateToHandler(err, buffer, handleLogoutResponse);
});
else {
logger.error('Invalid SAML logout request/response, query string: ' + JSON.stringify(req.query));
res.set('Content-Type', 'text/plain');
res.send('Invalid SAML logout request/response.');
}
}
catch (e) {
logException('Error while processing a SAML logout request/response, querystring=' + JSON.stringify(req.query) + ': ' + e, e);
try {
res.set('Content-Type', 'text/plain');
res.send('Error: ' + e);
}
catch (e2) {} // ignore
}
});
// ================================================================================
// Web API handlers
// ================================================================================
http_app.get('/saml/WaitLogin', function (req, res) {
var sid = req.query.sid,
waiter;
if (!sid) {
res.json({'code': -1, 'message': 'sid query parameter not set'});
return;
}
waiter = waitingLogins[sid];
if (!waiter) waitingLogins[sid] = waiter = makeWaiter(sid);
waiter.listeners = waiter.listeners + 1;
waiter.promise.then(function (jsonResponse) {
delete waitingLogins[sid];
res.json({'code': 0, 'message': 'OK', 'data': jsonResponse});
}, function (err) {
delete waitingLogins[sid];
res.json({'code': 1, 'message': '' + err});
}).done();
});
http_app.get('/saml/WaitLogout', function (req, res) {
var sid = req.query.sid,
saml = req.query.saml,
waiter, samlData;
if (!sid) {
res.json({'code': -1, 'message': 'sid query parameter not set'});
return;
}
if (!saml) {
res.json({'code': -2, 'message': 'saml query parameter not set'});
return;
}
try {
samlData = JSON.parse(saml);
}
catch (e) {
res.json({'code': -3, 'message': 'saml query parameter is not proper JSON: ' + e});
return;
}
waiter = waitingLogouts[sid];
if (!waiter) waitingLogouts[sid] = waiter = makeDoubleWaiter(sid);
waiter.listeners = waiter.listeners + 1;
waiter.resolve(samlData);
waiter.responsePromise.then(function () {
delete waitingLogouts[sid];
res.json({'code': 0, 'message': 'OK'});
}, function (err) {
delete waitingLogouts[sid];
res.json({'code': 1, 'message': '' + err});
}).done();
});
// ================================================================================
// A periodic cleaner: rejects and purges timed out requests
// ================================================================================
var cleanerTimer = setInterval(function () { // run every 10 seconds
var youngestTime = Date.now() - 300000, // request timeout: 5 minutes
keys = Object.keys(parkedLoginRequests),
i, key, waiter;
// parked logins: the user was already redirected at IdP for login
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
waiter = parkedLoginRequests[key];
if (waiter.parked < youngestTime) {
if (waiter.promise.isPending()) {
logger.warn('A login request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId)
waiter.promise.reject(new Error('Request timed out waiting for IdP login reply'));
}
else {
logger.info('Encountered a parked login request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId);
}
delete parkedLoginRequests[key];
delete waitingLogins[waiter.sid];
}
}
// waiting logins; the user's sid was registered, but the user itself may not have been redirected at IdP for login yet
keys = Object.keys(waitingLogins);
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
waiter = waitingLogins[key];
if ((waiter.parked || waiter.created) < youngestTime) {
if (waiter.promise.isPending()) {
if (waiter.parked) logger.warn('A login request, marked as parked but not encountered among parked requests, timed out at requester: sid=' + sid + ', requestId=' + waiter.requestId);
else logger.warn('A login request timed out at requester, with no outstanding request at IdP: sid=' + waiter.sid);
waiter.promise.reject(new Error('Request timed out waiting for user to start the login process'));
}
else {
logger.info('Encountered a waiting login request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId);
}
delete waitingLogins[key];
if (waiter.requestId) delete parkedLoginRequests[waiter.requestId];
}
}
// parked logouts: the user was already redirected at IdP for logout, if the registration data was already provided, otherwise it's still waiting for redirection
keys = Object.keys(parkedLogoutRequests);
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
waiter = parkedLogoutRequests[key];
if (waiter.parked < youngestTime) {
if (waiter.promise.isPending()) {
logger.warn('A logout request timed out waiting for registration info: sid=' + waiter.sid);
waiter.response.reject(new Error('Timed out waiting for registration info'));
if (waiter.responsePromise.isPending()) waiter.responsePromise.reject(new Error('Timed out waiting for registration info'));
}
else if (waiter.responsePromise.isPending()) {
logger.warn('A logout request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId)
waiter.responsePromise.reject(new Error('Request timed out waiting for IdP logout reply'));
}
else {
logger.info('Encountered a parked logout request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId);
}
delete parkedLogoutRequests[key];
delete waitingLogouts[waiter.sid];
}
}
// waiting logouts; the user's sid and registration data was registered, but the user itself may not have been redirected at IdP for logout yet
keys = Object.keys(waitingLogouts);
for (i = keys.length - 1; i >= 0; i--) {
key = keys[i];
waiter = waitingLogouts[key];
if ((waiter.parked || waiter.created) < youngestTime) {
if (waiter.promise.isPending()) {
if (waiter.parked) logger.warn('A logout request, marked as parked but not encountered among parked requests, timed out waiting for registration info: sid=' + sid + ', requestId=' + waiter.requestId); // this should be impossible: how can you go for logout at IdP if you have no registration info?
else logger.warn('A logout request timed out waiting for registration info, with no outstanding request at IdP: sid=' + waiter.sid);
waiter.response.reject(new Error('Timed out waiting for registration info'));
if (waiter.responsePromise.isPending()) waiter.responsePromise.reject(new Error('Timed out waiting for registration info'));
}
else if (waiter.responsePromise.isPending()) {
logger.warn('A logout request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId)
waiter.responsePromise.reject(new Error('Request timed out waiting for IdP logout reply'));
}
else {
logger.info('Encountered a waiting logout request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId);
}
delete waitingLogouts[key];
if (waiter.requestId) delete parkedLogoutRequests[waiter.requestId];
}
}
}, 10000);