diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000000..0622004d614 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,18 @@ +packages/rocketchat-livechat/ + +public/recorderWorker.js +lib/ua-parser.min.js +private/moment-locales/ +packages/autoupdate/ +packages/meteor-streams/ +packages/rocketchat-migrations/ +packages/rocketchat-katex/client/lib/katex.min.js +packages/rocketchat-favico/favico.js +packages/rocketchat-theme/client/minicolors/jquery.minicolors.js +packages/rocketchat-emojione/generateEmojiIndex.js +packages/rocketchat-ui/lib/Modernizr.js +packages/rocketchat-ui/lib/clipboardjs/clipboard.js +packages/rocketchat-ui/lib/jquery.swipebox.min.js +packages/rocketchat-ui/lib/particles.js +packages/rocketchat-ui/lib/recorderjs/recorder.js +packages/rocketchat-ui/lib/textarea-autogrow.js diff --git a/.jshintrc b/.jshintrc index 8aa6673dd81..59ab8238bd8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -81,35 +81,37 @@ // Custom Globals "globals" : { - "_" : true, - "Assets" : true, - "SHA256" : true, - "Accounts" : true, - "Blaze" : true, - "Email" : true, - "check" : true, - "crypto" : true, - "EJSON" : true, - "FlowRouter" : true, - "BlazeLayout" : true, - "Meteor" : true, - "Npm" : true, - "Package" : true, - "Promise" : true, // Avoid "redefinition of Promise" warning - "Random" : true, - "ReactiveVar" : true, - "RocketChat" : true, - "RocketChatFile" : true, - "RocketChatFileAvatarInstance" : true, - "s" : true, - "Session" : true, - "swal" : true, - "t" : true, - "TAPi18n" : true, - "Template" : true, - "TimeSync" : true, - "toastr" : true, - "Logger" : true, - "Tracker" : true + "_" : true, + "Accounts" : true, + "Assets" : true, + "Blaze" : true, + "BlazeLayout" : true, + "check" : true, + "crypto" : true, + "EJSON" : true, + "Email" : true, + "FlowRouter" : true, + "Logger" : true, + "Meteor" : true, + "Npm" : true, + "Package" : true, + "Promise" : true, // Avoid "redefinition of Promise" warning + "Random" : true, + "ReactiveVar" : true, + "RocketChat" : true, + "RocketChatFile" : true, + "RocketChatFileAvatarInstance": true, + "s" : true, + "ServiceConfiguration" : true, + "Session" : true, + "SHA256" : true, + "swal" : true, + "t" : true, + "TAPi18n" : true, + "Template" : true, + "TimeSync" : true, + "toastr" : true, + "Tracker" : true, + "WebApp" : true } } diff --git a/packages/meteor-accounts-saml/saml_client.js b/packages/meteor-accounts-saml/saml_client.js index a2a53389948..b4362511230 100644 --- a/packages/meteor-accounts-saml/saml_client.js +++ b/packages/meteor-accounts-saml/saml_client.js @@ -5,18 +5,19 @@ if (!Accounts.saml) { Accounts.saml.initiateLogin = function (options, callback, dimensions) { // default dimensions that worked well for facebook and google var popup = openCenteredPopup( - Meteor.absoluteUrl("_saml/authorize/" + options.provider + "/" + options.credentialToken), (dimensions && dimensions.width) || 650, (dimensions && dimensions.height) || 500); + Meteor.absoluteUrl('_saml/authorize/' + options.provider + '/' + options.credentialToken), (dimensions && dimensions.width) || 650, (dimensions && dimensions.height) || 500); var checkPopupOpen = setInterval(function () { + var popupClosed; try { // Fix for #328 - added a second test criteria (popup.closed === undefined) // to humour this Android quirk: // http://code.google.com/p/android/issues/detail?id=21061 - var popupClosed = popup.closed || popup.closed === undefined; + popupClosed = popup.closed || popup.closed === undefined; } catch (e) { // For some unknown reason, IE9 (and others?) sometimes (when - // the popup closes too quickly?) throws "SCRIPT16386: No such - // interface supported" when trying to read 'popup.closed'. Try + // the popup closes too quickly?) throws 'SCRIPT16386: No such + // interface supported' when trying to read 'popup.closed'. Try // again in 100ms. return; } @@ -44,8 +45,9 @@ var openCenteredPopup = function (url, width, height) { ',left=' + left + ',top=' + top + ',scrollbars=yes'); var newwindow = window.open(url, 'Login', features); - if (newwindow.focus) + if (newwindow.focus) { newwindow.focus(); + } return newwindow; }; @@ -54,7 +56,7 @@ Meteor.loginWithSaml = function (options, callback) { var credentialToken = Random.id(); options.credentialToken = credentialToken; - Accounts.saml.initiateLogin(options, function (error, result) { + Accounts.saml.initiateLogin(options, function (/*error, result*/) { Accounts.callLoginMethod({ methodArguments: [{ saml: true, @@ -65,12 +67,12 @@ Meteor.loginWithSaml = function (options, callback) { }); }; -Meteor.logoutWithSaml = function (options, callback) { - //Accounts.saml.idpInitiatedSLO(options, callback); - Meteor.call("samlLogout", options.provider, function (err, result) { - console.log("LOC " + result); +Meteor.logoutWithSaml = function (options/*, callback*/) { + //Accounts.saml.idpInitiatedSLO(options, callback); + Meteor.call('samlLogout', options.provider, function (err, result) { + console.log('LOC ' + result); // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. - //window.location.replace(Meteor.absoluteUrl("_saml/sloRedirect/" + options.provider + "/?redirect="+result)); - window.location.replace(Meteor.absoluteUrl("_saml/sloRedirect/" + options.provider + "/?redirect="+encodeURIComponent(result))); + //window.location.replace(Meteor.absoluteUrl('_saml/sloRedirect/' + options.provider + '/?redirect='+result)); + window.location.replace(Meteor.absoluteUrl('_saml/sloRedirect/' + options.provider + '/?redirect='+encodeURIComponent(result))); }); -}; \ No newline at end of file +}; diff --git a/packages/meteor-accounts-saml/saml_server.js b/packages/meteor-accounts-saml/saml_server.js index 1dd3f227201..0e0a37df69f 100644 --- a/packages/meteor-accounts-saml/saml_server.js +++ b/packages/meteor-accounts-saml/saml_server.js @@ -1,3 +1,5 @@ +/* globals RoutePolicy, SAML */ + if (!Accounts.saml) { Accounts.saml = { settings: { @@ -16,30 +18,31 @@ Meteor.methods({ samlLogout: function (provider) { // Make sure the user is logged in before initiate SAML SLO if (!Meteor.userId()) { - throw new Meteor.Error("not-authorized"); + throw new Meteor.Error('not-authorized'); } var samlProvider = function (element) { - return (element.provider == provider) - } - providerConfig = Accounts.saml.settings.providers.filter(samlProvider)[0]; + return (element.provider === provider); + }; + var providerConfig = Accounts.saml.settings.providers.filter(samlProvider)[0]; if (Accounts.saml.settings.debug) { - console.log("Logout request from " + JSON.stringify(providerConfig)); + console.log('Logout request from ' + JSON.stringify(providerConfig)); } // This query should respect upcoming array of SAML logins var user = Meteor.users.findOne({ _id: Meteor.userId(), - "services.saml.provider": provider + 'services.saml.provider': provider }, { - "services.saml": 1 + 'services.saml': 1 }); var nameID = user.services.saml.nameID; - var sessionIndex = nameID = user.services.saml.idpSession; + var sessionIndex = user.services.saml.idpSession; + nameID = sessionIndex; if (Accounts.saml.settings.debug) { - console.log("NameID for user " + Meteor.userId() + " found: " + JSON.stringify(nameID)); + console.log('NameID for user ' + Meteor.userId() + ' found: ' + JSON.stringify(nameID)); } - _saml = new SAML(providerConfig); + var _saml = new SAML(providerConfig); var request = _saml.generateLogoutRequest({ nameID: nameID, @@ -58,15 +61,15 @@ Meteor.methods({ }); var _syncRequestToUrl = Meteor.wrapAsync(_saml.requestToUrl, _saml); - var result = _syncRequestToUrl(request.request, "logout"); + var result = _syncRequestToUrl(request.request, 'logout'); if (Accounts.saml.settings.debug) { - console.log("SAML Logout Request " + result); + console.log('SAML Logout Request ' + result); } return result; } -}) +}); Accounts.registerLoginHandler(function (loginRequest) { if (!loginRequest.saml || !loginRequest.credentialToken) { @@ -75,14 +78,14 @@ Accounts.registerLoginHandler(function (loginRequest) { var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken); if (Accounts.saml.settings.debug) { - console.log("RESULT :" + JSON.stringify(loginResult)); + console.log('RESULT :' + JSON.stringify(loginResult)); } - if (loginResult == undefined) { + if (loginResult === undefined) { return { - type: "saml", - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, "No matching login attempt found") - } + type: 'saml', + error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found') + }; } if (loginResult && loginResult.profile && loginResult.profile.email) { @@ -142,10 +145,10 @@ Accounts.registerLoginHandler(function (loginRequest) { token: stampedToken.token }; - return result + return result; } else { - throw new Error("SAML Profile did not contain an email address"); + throw new Error('SAML Profile did not contain an email address'); } }); @@ -153,14 +156,14 @@ Accounts.saml._loginResultForCredentialToken = {}; Accounts.saml.hasCredential = function (credentialToken) { return _.has(Accounts.saml._loginResultForCredentialToken, credentialToken); -} +}; Accounts.saml.retrieveCredential = function (credentialToken) { // The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check. var result = Accounts.saml._loginResultForCredentialToken[credentialToken]; delete Accounts.saml._loginResultForCredentialToken[credentialToken]; return result; -} +}; // Listen to incoming SAML http requests @@ -182,61 +185,64 @@ middleware = function (req, res, next) { return; } - if (!samlObject.actionName) - throw new Error("Missing SAML action"); + if (!samlObject.actionName) { + throw new Error('Missing SAML action'); + } - console.log(Accounts.saml.settings.providers) - console.log(samlObject.serviceName) + console.log(Accounts.saml.settings.providers); + console.log(samlObject.serviceName); var service = _.find(Accounts.saml.settings.providers, function (samlSetting) { return samlSetting.provider === samlObject.serviceName; }); // Skip everything if there's no service set by the saml middleware - if (!service) - throw new Error("Unexpected SAML service " + samlObject.serviceName); + if (!service) { + throw new Error('Unexpected SAML service ' + samlObject.serviceName); + } + var _saml; switch (samlObject.actionName) { - case "metadata": + case 'metadata': _saml = new SAML(service); - service.callbackUrl = Meteor.absoluteUrl("_saml/validate/" + service.provider); + service.callbackUrl = Meteor.absoluteUrl('_saml/validate/' + service.provider); res.writeHead(200); res.write(_saml.generateServiceProviderMetadata(service.callbackUrl)); res.end(); //closePopup(res); break; - case "logout": + case 'logout': // This is where we receive SAML LogoutResponse _saml = new SAML(service); _saml.validateLogoutResponse(req.query.SAMLResponse, function (err, result) { if (!err) { var logOutUser = function (inResponseTo) { if (Accounts.saml.settings.debug) { - console.log("Logging Out user via inResponseTo " + inResponseTo); + console.log('Logging Out user via inResponseTo ' + inResponseTo); } var loggedOutUser = Meteor.users.find({ 'services.saml.inResponseTo': inResponseTo }).fetch(); - if (loggedOutUser.length == 1) { + if (loggedOutUser.length === 1) { if (Accounts.saml.settings.debug) { - console.log("Found user " + loggedOutUser[0]._id); + console.log('Found user ' + loggedOutUser[0]._id); } Meteor.users.update({ _id: loggedOutUser[0]._id }, { $set: { - "services.resume.loginTokens": [] + 'services.resume.loginTokens': [] } }); Meteor.users.update({ _id: loggedOutUser[0]._id }, { $unset: { - "services.saml": "" + 'services.saml': '' } }); } else { - throw new Meteor.error("Found multiple users matching SAML inResponseTo fields"); + throw new Meteor.error('Found multiple users matching SAML inResponseTo fields'); } - } + }; Fiber(function () { logOutUser(result); @@ -247,42 +253,46 @@ middleware = function (req, res, next) { 'Location': req.query.RelayState }); res.end(); - } else { - // TBD thinking of sth meaning full. } - }) + // else { + // // TBD thinking of sth meaning full. + // } + }); break; - case "sloRedirect": - var idpLogout = req.query.redirect + case 'sloRedirect': + var idpLogout = req.query.redirect; res.writeHead(302, { // credentialToken here is the SAML LogOut Request that we'll send back to IDP 'Location': idpLogout }); res.end(); break; - case "authorize": - service.callbackUrl = Meteor.absoluteUrl("_saml/validate/" + service.provider); + case 'authorize': + service.callbackUrl = Meteor.absoluteUrl('_saml/validate/' + service.provider); service.id = samlObject.credentialToken; _saml = new SAML(service); _saml.getAuthorizeUrl(req, function (err, url) { - if (err) - throw new Error("Unable to generate authorize url"); + if (err) { + throw new Error('Unable to generate authorize url'); + } res.writeHead(302, { 'Location': url }); res.end(); }); break; - case "validate": + case 'validate': _saml = new SAML(service); Accounts.saml.RelayState = req.body.RelayState; - _saml.validateResponse(req.body.SAMLResponse, req.body.RelayState, function (err, profile, loggedOut) { - if (err) - throw new Error("Unable to validate response url: " + err); + _saml.validateResponse(req.body.SAMLResponse, req.body.RelayState, function (err, profile/*, loggedOut*/) { + if (err) { + throw new Error('Unable to validate response url: ' + err); + } var credentialToken = profile.inResponseToId || profile.InResponseTo || samlObject.credentialToken; - if (!credentialToken) - throw new Error("Unable to determine credentialToken"); + if (!credentialToken) { + throw new Error('Unable to determine credentialToken'); + } Accounts.saml._loginResultForCredentialToken[credentialToken] = { profile: profile }; @@ -290,7 +300,7 @@ middleware = function (req, res, next) { }); break; default: - throw new Error("Unexpected SAML action " + samlObject.actionName); + throw new Error('Unexpected SAML action ' + samlObject.actionName); } } catch (err) { @@ -299,16 +309,18 @@ middleware = function (req, res, next) { }; var samlUrlToObject = function (url) { - // req.url will be "/_saml///" - if (!url) + // req.url will be '/_saml///' + if (!url) { return null; + } var splitPath = url.split('/'); // Any non-saml request will continue down the default // middlewares. - if (splitPath[1] !== '_saml') + if (splitPath[1] !== '_saml') { return null; + } var result = { actionName: splitPath[2], @@ -326,7 +338,8 @@ var closePopup = function (res, err) { 'Content-Type': 'text/html' }); var content = '

Verified

'; - if (err) + if (err) { content = '

Sorry, an annoying error occured

' + err + '
Close Window'; + } res.end(content, 'utf-8'); }; diff --git a/packages/meteor-accounts-saml/saml_utils.js b/packages/meteor-accounts-saml/saml_utils.js index c90734a6451..12efd6a50fa 100644 --- a/packages/meteor-accounts-saml/saml_utils.js +++ b/packages/meteor-accounts-saml/saml_utils.js @@ -1,3 +1,5 @@ +/* globals SAML:true */ + var zlib = Npm.require('zlib'); var xml2js = Npm.require('xml2js'); var xmlCrypto = Npm.require('xml-crypto'); @@ -5,20 +7,20 @@ var crypto = Npm.require('crypto'); var xmldom = Npm.require('xmldom'); var querystring = Npm.require('querystring'); var xmlbuilder = Npm.require('xmlbuilder'); -var xmlenc = Npm.require('xml-encryption'); -var xpath = xmlCrypto.xpath; -var Dom = xmldom.DOMParser; +// var xmlenc = Npm.require('xml-encryption'); +// var xpath = xmlCrypto.xpath; +// var Dom = xmldom.DOMParser; -var prefixMatch = new RegExp(/(?!xmlns)^.*:/); +// var prefixMatch = new RegExp(/(?!xmlns)^.*:/); SAML = function (options) { this.options = this.initialize(options); }; -var stripPrefix = function (str) { - return str.replace(prefixMatch, ''); -}; +// var stripPrefix = function (str) { +// return str.replace(prefixMatch, ''); +// }; SAML.prototype.initialize = function (options) { if (!options) { @@ -38,19 +40,19 @@ SAML.prototype.initialize = function (options) { } if (options.identifierFormat === undefined) { - options.identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; + options.identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'; } if (options.authnContext === undefined) { - options.authnContext = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"; + options.authnContext = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; } return options; }; SAML.prototype.generateUniqueID = function () { - var chars = "abcdef0123456789"; - var uniqueID = ""; + var chars = 'abcdef0123456789'; + var uniqueID = ''; for (var i = 0; i < 20; i++) { uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1); } @@ -65,37 +67,39 @@ SAML.prototype.signRequest = function (xml) { var signer = crypto.createSign('RSA-SHA1'); signer.update(xml); return signer.sign(this.options.privateKey, 'base64'); -} +}; SAML.prototype.generateAuthorizeRequest = function (req) { - var id = "_" + this.generateUniqueID(); + var id = '_' + this.generateUniqueID(); var instant = this.generateInstant(); // Post-auth destination + var callbackUrl; if (this.options.callbackUrl) { callbackUrl = this.options.callbackUrl; } else { - var callbackUrl = this.options.protocol + req.headers.host + this.options.path; + callbackUrl = this.options.protocol + req.headers.host + this.options.path; } - if (this.options.id) + if (this.options.id) { id = this.options.id; + } var request = - "" + - "" + this.options.issuer + "\n"; + '' + + '' + this.options.issuer + '\n'; if (this.options.identifierFormat) { - request += "\n"; + request += '\n'; } request += - "" + - "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n" + - ""; + '' + + 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport\n' + + ''; return request; }; @@ -106,39 +110,39 @@ SAML.prototype.generateLogoutRequest = function (options) { // sessionIndex: sessionIndex // --- NO SAMLsettings: " + - "" + this.options.issuer + "" + - "" + options.nameID + "" + - ""; - - request = "" + - "" + this.options.issuer + "" + - "" + - options.nameID + "" + - "" + options.sessionIndex + "" + - ""; + var request = '' + + '' + this.options.issuer + '' + + '' + options.nameID + '' + + ''; + + request = '' + + '' + this.options.issuer + '' + + '' + + options.nameID + '' + + '' + options.sessionIndex + '' + + ''; if (Meteor.settings.debug) { - console.log("------- SAML Logout request -----------"); + console.log('------- SAML Logout request -----------'); console.log(request); } return { request: request, id: id }; -} +}; SAML.prototype.requestToUrl = function (request, operation, callback) { var self = this; @@ -157,10 +161,11 @@ SAML.prototype.requestToUrl = function (request, operation, callback) { } } - if (target.indexOf('?') > 0) + if (target.indexOf('?') > 0) { target += '&'; - else + } else { target += '?'; + } var samlRequest = { SAMLRequest: base64 @@ -172,16 +177,17 @@ SAML.prototype.requestToUrl = function (request, operation, callback) { } // TBD. We should really include a proper RelayState here + var relayState; if (operation === 'logout') { // in case of logout we want to be redirected back to the Meteor app. - var relayState = Meteor.absoluteUrl(); + relayState = Meteor.absoluteUrl(); } else { - var relayState = self.options.provider; + relayState = self.options.provider; } - target += querystring.stringify(samlRequest) + "&RelayState=" + relayState; + target += querystring.stringify(samlRequest) + '&RelayState=' + relayState; if (Meteor.settings.debug) { - console.log("requestToUrl: " + target); + console.log('requestToUrl: ' + target); } if (operation === 'logout') { // in case of logout we want to be redirected back to the Meteor app. @@ -192,7 +198,7 @@ SAML.prototype.requestToUrl = function (request, operation, callback) { callback(null, target); } }); -} +}; SAML.prototype.getAuthorizeUrl = function (req, callback) { var request = this.generateAuthorizeRequest(req); @@ -204,39 +210,39 @@ SAML.prototype.getLogoutUrl = function (req, callback) { var request = this.generateLogoutRequest(req); this.requestToUrl(request, 'logout', callback); -} +}; SAML.prototype.certToPEM = function (cert) { cert = cert.match(/.{1,64}/g).join('\n'); - cert = "-----BEGIN CERTIFICATE-----\n" + cert; - cert = cert + "\n-----END CERTIFICATE-----\n"; + cert = '-----BEGIN CERTIFICATE-----\n' + cert; + cert = cert + '\n-----END CERTIFICATE-----\n'; return cert; }; -function findChilds(node, localName, namespace) { - var res = [] - for (var i = 0; i < node.childNodes.length; i++) { - var child = node.childNodes[i] - if (child.localName == localName && (child.namespaceURI == namespace || !namespace)) { - res.push(child) - } - } - return res; -} +// function findChilds(node, localName, namespace) { +// var res = []; +// for (var i = 0; i < node.childNodes.length; i++) { +// var child = node.childNodes[i]; +// if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) { +// res.push(child); +// } +// } +// return res; +// } SAML.prototype.validateSignature = function (xml, cert) { var self = this; var doc = new xmldom.DOMParser().parseFromString(xml); - var signature = xmlCrypto.xpath(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0]; + var signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0]; var sig = new xmlCrypto.SignedXml(); sig.keyInfoProvider = { - getKeyInfo: function (key) { - return "" + getKeyInfo: function (/*key*/) { + return ''; }, - getKey: function (keyInfo) { + getKey: function (/*keyInfo*/) { return self.certToPEM(cert); } }; @@ -257,7 +263,7 @@ SAML.prototype.getElement = function (parentElement, elementName) { return parentElement['saml2:' + elementName]; } return parentElement[elementName]; -} +}; SAML.prototype.validateLogoutResponse = function (samlResponse, callback) { var self = this; @@ -267,7 +273,7 @@ SAML.prototype.validateLogoutResponse = function (samlResponse, callback) { if (err) { if (Meteor.settings.debug) { - console.log(err) + console.log(err); } } else { var parser = new xml2js.Parser({ @@ -278,37 +284,37 @@ SAML.prototype.validateLogoutResponse = function (samlResponse, callback) { if (response) { // TBD. Check if this msg corresponds to one we sent - var inResponseTo = response['$'].InResponseTo; + var inResponseTo = response.$.InResponseTo; if (Meteor.settings.debug) { - console.log("In Response to: " + inResponseTo); + console.log('In Response to: ' + inResponseTo); } var status = self.getElement(response, 'Status'); - var statusCode = self.getElement(status[0], 'StatusCode')[0]['$'].Value; + var statusCode = self.getElement(status[0], 'StatusCode')[0].$.Value; if (Meteor.settings.debug) { - console.log("StatusCode: " + JSON.stringify(statusCode)); + console.log('StatusCode: ' + JSON.stringify(statusCode)); } if (statusCode === 'urn:oasis:names:tc:SAML:2.0:status:Success') { // In case of a successful logout at IDP we return inResponseTo value. // This is the only way how we can identify the Meteor user (as we don't use Session Cookies) callback(null, inResponseTo); } else { - callback("Error. Logout not confirmed by IDP", null); + callback('Error. Logout not confirmed by IDP', null); } } else { - callback("No Response Found", null); + callback('No Response Found', null); } - }) + }); } - }) -} + }); +}; SAML.prototype.validateResponse = function (samlResponse, relayState, callback) { var self = this; var xml = new Buffer(samlResponse, 'base64').toString('ascii'); // We currently use RelayState to save SAML provider if (Meteor.settings.debug) { - console.log("Validating response with relay state: " + xml); + console.log('Validating response with relay state: ' + xml); } var parser = new xml2js.Parser({ explicitRoot: true @@ -317,20 +323,20 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) parser.parseString(xml, function (err, doc) { // Verify signature if (Meteor.settings.debug) { - console.log("Verify signature"); + console.log('Verify signature'); } if (self.options.cert && !self.validateSignature(xml, self.options.cert)) { if (Meteor.settings.debug) { - console.log("Signature WRONG"); + console.log('Signature WRONG'); } return callback(new Error('Invalid signature'), null, false); } if (Meteor.settings.debug) { - console.log("Signature OK"); + console.log('Signature OK'); } var response = self.getElement(doc, 'Response'); if (Meteor.settings.debug) { - console.log("Got response"); + console.log('Got response'); } if (response) { var assertion = self.getElement(response, 'Assertion'); @@ -338,10 +344,10 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) return callback(new Error('Missing SAML assertion'), null, false); } - profile = {}; + var profile = {}; - if (response['$'] && response['$']['InResponseTo']) { - profile.inResponseToId = response['$']['InResponseTo']; + if (response.$ && response.$.InResponseTo) { + profile.inResponseToId = response.$.InResponseTo; } var issuer = self.getElement(assertion[0], 'Issuer'); @@ -354,10 +360,10 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) if (subject) { var nameID = self.getElement(subject[0], 'NameID'); if (nameID) { - profile.nameID = nameID[0]["_"]; + profile.nameID = nameID[0]._; - if (nameID[0]['$'].Format) { - profile.nameIDFormat = nameID[0]['$'].Format; + if (nameID[0].$.Format) { + profile.nameIDFormat = nameID[0].$.Format; } } } @@ -365,22 +371,22 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) var authnStatement = self.getElement(assertion[0], 'AuthnStatement'); if (authnStatement) { - if (authnStatement[0]['$'].SessionIndex) { + if (authnStatement[0].$.SessionIndex) { - profile.sessionIndex = authnStatement[0]['$'].SessionIndex; + profile.sessionIndex = authnStatement[0].$.SessionIndex; if (Meteor.settings.debug) { - console.log("Session Index: " + profile.sessionIndex); + console.log('Session Index: ' + profile.sessionIndex); } } else { if (Meteor.settings.debug) { - console.log("No Session Index Found"); + console.log('No Session Index Found'); } } } else { if (Meteor.settings.debug) { - console.log("No AuthN Statement found"); + console.log('No AuthN Statement found'); } } @@ -392,9 +398,9 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) attributes.forEach(function (attribute) { var value = self.getElement(attribute, 'AttributeValue'); if (typeof value[0] === 'string') { - profile[attribute['$'].Name] = value[0]; + profile[attribute.$.Name] = value[0]; } else { - profile[attribute['$'].Name] = value[0]['_']; + profile[attribute.$.Name] = value[0]._; } }); } @@ -413,7 +419,7 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) profile.email = profile.nameID; } if (Meteor.settings.debug) { - console.log("NameID: " + JSON.stringify(profile)); + console.log('NameID: ' + JSON.stringify(profile)); } callback(null, profile, false); @@ -430,7 +436,7 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback) }); }; - +var decryptionCert; SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) { var keyDescriptor = null; @@ -442,7 +448,7 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) { if (this.options.privateKey) { if (!decryptionCert) { throw new Error( - "Missing decryptionCert while generating metadata for decrypting service provider"); + 'Missing decryptionCert while generating metadata for decrypting service provider'); } decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, ''); @@ -480,7 +486,7 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) { if (!this.options.callbackUrl && !callbackUrl) { throw new Error( - "Unable to generate service provider metadata when callbackUrl option is not set"); + 'Unable to generate service provider metadata when callbackUrl option is not set'); } var metadata = { @@ -493,8 +499,8 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) { 'KeyDescriptor': keyDescriptor, 'SingleLogoutService': { '@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - '@Location': Meteor.absoluteUrl() + "_saml/logout/" + this.options.provider + "/", - '@ResponseLocation': Meteor.absoluteUrl() + "_saml/logout/" + this.options.provider + "/" + '@Location': Meteor.absoluteUrl() + '_saml/logout/' + this.options.provider + '/', + '@ResponseLocation': Meteor.absoluteUrl() + '_saml/logout/' + this.options.provider + '/' }, 'NameIDFormat': this.options.identifierFormat, 'AssertionConsumerService': { @@ -512,4 +518,4 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) { indent: ' ', newline: '\n' }); -}; \ No newline at end of file +}; diff --git a/packages/rocketchat-cas/cas_client.js b/packages/rocketchat-cas/cas_client.js index 454eb3c6b98..d391b942865 100644 --- a/packages/rocketchat-cas/cas_client.js +++ b/packages/rocketchat-cas/cas_client.js @@ -1,67 +1,68 @@ Meteor.loginWithCas = function(callback) { - var credentialToken = Random.id(); - var login_url = RocketChat.settings.get("CAS_login_url"); - var popup_width = RocketChat.settings.get("CAS_popup_width"); - var popup_height = RocketChat.settings.get("CAS_popup_height"); + var credentialToken = Random.id(); + var login_url = RocketChat.settings.get('CAS_login_url'); + var popup_width = RocketChat.settings.get('CAS_popup_width'); + var popup_height = RocketChat.settings.get('CAS_popup_height'); - if (!login_url) { - return; - } + if (!login_url) { + return; + } - var loginUrl = login_url + "?service=" + Meteor.absoluteUrl('_cas/') + credentialToken; + var loginUrl = login_url + '?service=' + Meteor.absoluteUrl('_cas/') + credentialToken; - var popup = openCenteredPopup( - loginUrl, - popup_width || 800, - popup_height || 600 - ); + var popup = openCenteredPopup( + loginUrl, + popup_width || 800, + popup_height || 600 + ); - var checkPopupOpen = setInterval(function() { - try { - // Fix for #328 - added a second test criteria (popup.closed === undefined) - // to humour this Android quirk: - // http://code.google.com/p/android/issues/detail?id=21061 - var popupClosed = popup.closed || popup.closed === undefined; - } catch (e) { - // For some unknown reason, IE9 (and others?) sometimes (when - // the popup closes too quickly?) throws "SCRIPT16386: No such - // interface supported" when trying to read 'popup.closed'. Try - // again in 100ms. - return; - } + var checkPopupOpen = setInterval(function() { + var popupClosed; + try { + // Fix for #328 - added a second test criteria (popup.closed === undefined) + // to humour this Android quirk: + // http://code.google.com/p/android/issues/detail?id=21061 + popupClosed = popup.closed || popup.closed === undefined; + } catch (e) { + // For some unknown reason, IE9 (and others?) sometimes (when + // the popup closes too quickly?) throws "SCRIPT16386: No such + // interface supported" when trying to read 'popup.closed'. Try + // again in 100ms. + return; + } - if (popupClosed) { - clearInterval(checkPopupOpen); + if (popupClosed) { + clearInterval(checkPopupOpen); - // check auth on server. - Accounts.callLoginMethod({ - methodArguments: [{ cas: { credentialToken: credentialToken } }], - userCallback: callback - }); - } - }, 100); + // check auth on server. + Accounts.callLoginMethod({ + methodArguments: [{ cas: { credentialToken: credentialToken } }], + userCallback: callback + }); + } + }, 100); }; var openCenteredPopup = function(url, width, height) { - var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; - var screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop; - var outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth; - var outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22); - // XXX what is the 22? + var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; + var screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop; + var outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth; + var outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22); + // XXX what is the 22? - // Use `outerWidth - width` and `outerHeight - height` for help in - // positioning the popup centered relative to the current window - var left = screenX + (outerWidth - width) / 2; - var top = screenY + (outerHeight - height) / 2; - var features = ('width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes'); + // Use `outerWidth - width` and `outerHeight - height` for help in + // positioning the popup centered relative to the current window + var left = screenX + (outerWidth - width) / 2; + var top = screenY + (outerHeight - height) / 2; + var features = ('width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes'); - var newwindow = window.open(url, 'Login', features); - if (newwindow.focus) { - newwindow.focus(); - } + var newwindow = window.open(url, 'Login', features); + if (newwindow.focus) { + newwindow.focus(); + } - return newwindow; + return newwindow; }; diff --git a/packages/rocketchat-cas/cas_rocketchat.js b/packages/rocketchat-cas/cas_rocketchat.js index 1446b83aa11..a25186e1442 100644 --- a/packages/rocketchat-cas/cas_rocketchat.js +++ b/packages/rocketchat-cas/cas_rocketchat.js @@ -1,64 +1,66 @@ +/* globals logger:true */ + logger = new Logger('CAS', {}); Meteor.startup(function(){ - RocketChat.settings.addGroup('CAS', function() { - this.add("CAS_enabled", false, { type: 'boolean', group: 'CAS', public: true }); - this.add("CAS_base_url" , '' , { type: 'string' , group: 'CAS', public: true }); - this.add("CAS_login_url" , '' , { type: 'string' , group: 'CAS', public: true }); - this.add("CAS_version" , '1.0' , { type: 'select', values: [{ key: '1.0', i18nLabel: '1.0'}], group: 'CAS' }); + RocketChat.settings.addGroup('CAS', function() { + this.add('CAS_enabled', false, { type: 'boolean', group: 'CAS', public: true }); + this.add('CAS_base_url' , '' , { type: 'string' , group: 'CAS', public: true }); + this.add('CAS_login_url' , '' , { type: 'string' , group: 'CAS', public: true }); + this.add('CAS_version' , '1.0' , { type: 'select', values: [{ key: '1.0', i18nLabel: '1.0'}], group: 'CAS' }); - this.section('CAS Login Layout', function() { - this.add("CAS_popup_width" , '810' , { type: 'string' , group: 'CAS', public: true }); - this.add("CAS_popup_height" , '610' , { type: 'string' , group: 'CAS', public: true }); - this.add("CAS_button_label_text" , 'CAS' , { type: 'string' , group: 'CAS'}); - this.add("CAS_button_label_color", '#FFFFFF' , { type: 'color' , group: 'CAS'}); - this.add("CAS_button_color" , '#13679A' , { type: 'color' , group: 'CAS'}); - this.add("CAS_autoclose", true , { type: 'boolean' , group: 'CAS'}); - }); - }); + this.section('CAS Login Layout', function() { + this.add('CAS_popup_width' , '810' , { type: 'string' , group: 'CAS', public: true }); + this.add('CAS_popup_height' , '610' , { type: 'string' , group: 'CAS', public: true }); + this.add('CAS_button_label_text' , 'CAS' , { type: 'string' , group: 'CAS'}); + this.add('CAS_button_label_color', '#FFFFFF' , { type: 'color' , group: 'CAS'}); + this.add('CAS_button_color' , '#13679A' , { type: 'color' , group: 'CAS'}); + this.add('CAS_autoclose', true , { type: 'boolean' , group: 'CAS'}); + }); + }); }); - -timer = undefined -function updateServices(record) { - if( typeof timer != 'undefined' ) { - Meteor.clearTimeout(timer); - } +var timer; + +function updateServices(/*record*/) { + if( typeof timer !== 'undefined' ) { + Meteor.clearTimeout(timer); + } - timer = Meteor.setTimeout(function() { - data = { - // These will pe passed to 'node-cas' as options - enabled: RocketChat.settings.get("CAS_enabled"), - base_url: RocketChat.settings.get("CAS_base_url"), - login_url: RocketChat.settings.get("CAS_login_url"), - // Rocketchat Visuals - buttonLabelText: RocketChat.settings.get("CAS_button_label_text"), - buttonLabelColor: RocketChat.settings.get("CAS_button_label_color"), - buttonColor: RocketChat.settings.get("CAS_button_color"), - width: RocketChat.settings.get("CAS_popup_width"), - height: RocketChat.settings.get("CAS_popup_height"), - autoclose: RocketChat.settings.get("CAS_autoclose"), - }; + timer = Meteor.setTimeout(function() { + var data = { + // These will pe passed to 'node-cas' as options + enabled: RocketChat.settings.get('CAS_enabled'), + base_url: RocketChat.settings.get('CAS_base_url'), + login_url: RocketChat.settings.get('CAS_login_url'), + // Rocketchat Visuals + buttonLabelText: RocketChat.settings.get('CAS_button_label_text'), + buttonLabelColor: RocketChat.settings.get('CAS_button_label_color'), + buttonColor: RocketChat.settings.get('CAS_button_color'), + width: RocketChat.settings.get('CAS_popup_width'), + height: RocketChat.settings.get('CAS_popup_height'), + autoclose: RocketChat.settings.get('CAS_autoclose'), + }; - // Either register or deregister the CAS login service based upon its configuration - if( data.enabled ) { - logger.info("Enabling CAS login service") - ServiceConfiguration.configurations.upsert({service: 'cas'}, { $set: data }); - } else { - logger.info("Disabling CAS login service"); - ServiceConfiguration.configurations.remove({service: 'cas'}); - } - }, 2000); -}; + // Either register or deregister the CAS login service based upon its configuration + if( data.enabled ) { + logger.info('Enabling CAS login service'); + ServiceConfiguration.configurations.upsert({service: 'cas'}, { $set: data }); + } else { + logger.info('Disabling CAS login service'); + ServiceConfiguration.configurations.remove({service: 'cas'}); + } + }, 2000); +} function check_record (record) { - if( /^CAS_.+/.test( record._id )){ - updateServices( record ); - } -}; + if( /^CAS_.+/.test( record._id )){ + updateServices( record ); + } +} RocketChat.models.Settings.find().observe({ - added: check_record, - changed: check_record, - removed: check_record + added: check_record, + changed: check_record, + removed: check_record }); diff --git a/packages/rocketchat-cas/cas_server.js b/packages/rocketchat-cas/cas_server.js index 858882752fa..fa3b702b602 100644 --- a/packages/rocketchat-cas/cas_server.js +++ b/packages/rocketchat-cas/cas_server.js @@ -1,3 +1,5 @@ +/* globals RoutePolicy, logger */ + var Fiber = Npm.require('fibers'); var url = Npm.require('url'); var CAS = Npm.require('cas'); @@ -8,80 +10,80 @@ RoutePolicy.declare('/_cas/', 'network'); // Listen to incoming OAuth http requests WebApp.connectHandlers.use(function(req, res, next) { - // Need to create a Fiber since we're using synchronous http calls and nothing - // else is wrapping this in a fiber automatically - Fiber(function () { - middleware(req, res, next); - }).run(); + // Need to create a Fiber since we're using synchronous http calls and nothing + // else is wrapping this in a fiber automatically + Fiber(function () { + middleware(req, res, next); + }).run(); }); var middleware = function (req, res, next) { - // Make sure to catch any exceptions because otherwise we'd crash - // the runner - try { - var barePath = req.url.substring(0, req.url.indexOf('?')); - var splitPath = barePath.split('/'); - - // Any non-cas request will continue down the default - // middlewares. - if (splitPath[1] !== '_cas') { - next(); - return; - } - - // get auth token - var credentialToken = splitPath[2]; - if (!credentialToken) { - closePopup(res); - return; - } - - // validate ticket - casTicket(req, credentialToken, function() { - closePopup(res); - }); - - } catch (err) { - logger.error("Unexpected error : " + err.message); - closePopup(res); - } + // Make sure to catch any exceptions because otherwise we'd crash + // the runner + try { + var barePath = req.url.substring(0, req.url.indexOf('?')); + var splitPath = barePath.split('/'); + + // Any non-cas request will continue down the default + // middlewares. + if (splitPath[1] !== '_cas') { + next(); + return; + } + + // get auth token + var credentialToken = splitPath[2]; + if (!credentialToken) { + closePopup(res); + return; + } + + // validate ticket + casTicket(req, credentialToken, function() { + closePopup(res); + }); + + } catch (err) { + logger.error('Unexpected error : ' + err.message); + closePopup(res); + } }; var casTicket = function (req, token, callback) { - // get configuration - if (!RocketChat.settings.get("CAS_enabled")) { - logger.error("Got ticket validation request, but CAS is not enabled"); - callback(); - } - - // get ticket and validate. - var parsedUrl = url.parse(req.url, true); - var ticketId = parsedUrl.query.ticket; - var baseUrl = RocketChat.settings.get("CAS_base_url"); - logger.debug("Using CAS_base_url: " + baseUrl); - - var cas = new CAS({ - base_url: baseUrl, - service: Meteor.absoluteUrl() + "_cas/" + token - }); - - cas.validate(ticketId, function(err, status, username) { - if (err) { - logger.error("error when trying to validate " + err); - } else { - if (status) { - logger.info("Validated user: " + username); - _casCredentialTokens[token] = { id: username }; - } else { - logger.error("Unable to validate ticket: " + ticketId); - } - } - - callback(); - }); - - return; + // get configuration + if (!RocketChat.settings.get('CAS_enabled')) { + logger.error('Got ticket validation request, but CAS is not enabled'); + callback(); + } + + // get ticket and validate. + var parsedUrl = url.parse(req.url, true); + var ticketId = parsedUrl.query.ticket; + var baseUrl = RocketChat.settings.get('CAS_base_url'); + logger.debug('Using CAS_base_url: ' + baseUrl); + + var cas = new CAS({ + base_url: baseUrl, + service: Meteor.absoluteUrl() + '_cas/' + token + }); + + cas.validate(ticketId, function(err, status, username) { + if (err) { + logger.error('error when trying to validate ' + err); + } else { + if (status) { + logger.info('Validated user: ' + username); + _casCredentialTokens[token] = { id: username }; + } else { + logger.error('Unable to validate ticket: ' + ticketId); + } + } + + callback(); + }); + + return; }; /* @@ -89,72 +91,73 @@ var casTicket = function (req, token, callback) { * It is call after Accounts.callLoginMethod() is call from client. * */ - Accounts.registerLoginHandler(function (options) { - - if (!options.cas) - return undefined; - - if (!_hasCredential(options.cas.credentialToken)) { - throw new Meteor.Error(Accounts.LoginCancelledError.numericError, - 'no matching login attempt found'); - } - - var result = _retrieveCredential(options.cas.credentialToken); - var options = { profile: { name: result.id } }; - - // Search existing user by its external service id - logger.debug("Looking up user with username: " + result.id ); - var user = Meteor.users.findOne({ 'services.cas.external_id': result.id }); - - if (user) { - logger.debug("Using existing user for '" + result.id + "' with id: " + user._id); - } else { - - // Define new user - var newUser = { - username: result.id, - active: true, - globalRoles: ['user'], - services: { - cas: { - external_id: result.id - } - } - }; - - // Create the user - logger.debug("User '" + result.id + "'does not exist yet, creating it"); - var userId = Accounts.insertUserDoc({}, newUser); - - // Fetch and use it - user = Meteor.users.findOne(userId); - logger.debug("Created new user for '" + result.id + "' with id: " + user._id); - - logger.debug('Joining user to default channels'); - Meteor.runAsUser(user._id, function() { - Meteor.call('joinDefaultChannels'); - }); - - } - - return { userId: user._id }; +Accounts.registerLoginHandler(function (options) { + + if (!options.cas) { + return undefined; + } + + if (!_hasCredential(options.cas.credentialToken)) { + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, + 'no matching login attempt found'); + } + + var result = _retrieveCredential(options.cas.credentialToken); + options = { profile: { name: result.id } }; + + // Search existing user by its external service id + logger.debug('Looking up user with username: ' + result.id ); + var user = Meteor.users.findOne({ 'services.cas.external_id': result.id }); + + if (user) { + logger.debug('Using existing user for \'' + result.id + '\' with id: ' + user._id); + } else { + + // Define new user + var newUser = { + username: result.id, + active: true, + globalRoles: ['user'], + services: { + cas: { + external_id: result.id + } + } + }; + + // Create the user + logger.debug('User \'' + result.id + '\'does not exist yet, creating it'); + var userId = Accounts.insertUserDoc({}, newUser); + + // Fetch and use it + user = Meteor.users.findOne(userId); + logger.debug('Created new user for \'' + result.id + '\' with id: ' + user._id); + + logger.debug('Joining user to default channels'); + Meteor.runAsUser(user._id, function() { + Meteor.call('joinDefaultChannels'); + }); + + } + + return { userId: user._id }; }); var _hasCredential = function(credentialToken) { - return _.has(_casCredentialTokens, credentialToken); -} + return _.has(_casCredentialTokens, credentialToken); +}; /* * Retrieve token and delete it to avoid replaying it. */ var _retrieveCredential = function(credentialToken) { - var result = _casCredentialTokens[credentialToken]; - delete _casCredentialTokens[credentialToken]; - return result; -} + var result = _casCredentialTokens[credentialToken]; + delete _casCredentialTokens[credentialToken]; + return result; +}; var closePopup = function(res) { - res.writeHead(200, {'Content-Type': 'text/html'}); - var content = ''; - res.end(content, 'utf-8'); -} + res.writeHead(200, {'Content-Type': 'text/html'}); + var content = ''; + res.end(content, 'utf-8'); +}; diff --git a/packages/rocketchat-cas/package.js b/packages/rocketchat-cas/package.js index 332a1d166a9..11daa21cb7b 100644 --- a/packages/rocketchat-cas/package.js +++ b/packages/rocketchat-cas/package.js @@ -1,8 +1,8 @@ Package.describe({ - name: "rocketchat:cas", - summary: "CAS support for accounts", - version: "1.0.0", - git: "https://github.com/rocketchat/rocketchat-cas" + name: 'rocketchat:cas', + summary: 'CAS support for accounts', + version: '1.0.0', + git: 'https://github.com/rocketchat/rocketchat-cas' }); Package.onUse(function(api) { @@ -18,6 +18,7 @@ Package.onUse(function(api) { api.use('accounts-base', 'server'); api.use('underscore'); + api.use('ecmaescript'); // Server files api.add_files('cas_rocketchat.js', 'server'); @@ -29,5 +30,5 @@ Package.onUse(function(api) { }); Npm.depends({ - cas: "0.0.3" + cas: '0.0.3' });