| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 | "use strict";const Fiber = Npm.require('fibers');const https = Npm.require('https');const url = Npm.require('url');const xmlParser = Npm.require('xml2js');// Libraryclass CAS {  constructor(options) {    options = options || {};    if (!options.validate_url) {      throw new Error('Required CAS option `validateUrl` missing.');    }    if (!options.service) {      throw new Error('Required CAS option `service` missing.');    }    const cas_url = url.parse(options.validate_url);    if (cas_url.protocol != 'https:' ) {      throw new Error('Only https CAS servers are supported.');    } else if (!cas_url.hostname) {      throw new Error('Option `validateUrl` must be a valid url like: https://example.com/cas/serviceValidate');    } else {      this.hostname = cas_url.host;      this.port = 443;// Should be 443 for https      this.validate_path = cas_url.pathname;    }    this.service = options.service;  }  validate(ticket, callback) {    const httparams = {      host: this.hostname,      port: this.port,      path: url.format({        pathname: this.validate_path,        query: {ticket: ticket, service: this.service},      }),    };    https.get(httparams, (res) => {      res.on('error', (e) => {        console.log('error' + e);        callback(e);      });      // Read result      res.setEncoding('utf8');      let response = '';      res.on('data', (chunk) => {        response += chunk;      });      res.on('end', (error) => {        if (error) {          console.log('error callback');          console.log(error);          callback(undefined, false);        } else {          xmlParser.parseString(response, (err, result) => {            if (err) {              console.log('Bad response format.');              callback({message: 'Bad response format. XML could not parse it'});            } else {              if (result['cas:serviceResponse'] == null) {                console.log('Empty response.');                callback({message: 'Empty response.'});              }              if (result['cas:serviceResponse']['cas:authenticationSuccess']) {                const userData = {                  id: result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:user'][0].toLowerCase(),                };                const attributes = result['cas:serviceResponse']['cas:authenticationSuccess'][0]['cas:attributes'][0];                // Check allowed ldap groups if exist (array only)                // example cas settings : "allowedLdapGroups" : ["wekan", "admin"],                let findedGroup = false;                const allowedLdapGroups = Meteor.settings.cas.allowedLdapGroups || false;                for (const fieldName in attributes) {                  if (allowedLdapGroups && fieldName === 'cas:memberOf') {                    for (const groups in attributes[fieldName]) {                      const str = attributes[fieldName][groups];                      if (!Array.isArray(allowedLdapGroups)) {                        callback({message: 'Settings "allowedLdapGroups" must be an array'});                      }                      for (const allowedLdapGroup in allowedLdapGroups) {                        if (str.search(`cn=${allowedLdapGroups[allowedLdapGroup]}`) >= 0) {                          findedGroup = true;                        }                      }                    }                  }                  userData[fieldName] = attributes[fieldName][0];                }                if (allowedLdapGroups && !findedGroup) {                  callback({message: 'Group not finded.'}, false);                } else {                  callback(undefined, true, userData);                }              } else {                callback(undefined, false);              }            }          });        }      });    });  }}////// END OF CAS MODULElet _casCredentialTokens = {};let _userData = {};//RoutePolicy.declare('/_cas/', 'network');// Listen to incoming OAuth http requestsWebApp.connectHandlers.use((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(() => {    middleware(req, res, next);  }).run();});const middleware = (req, res, next) => {  // Make sure to catch any exceptions because otherwise we'd crash  // the runner  try {    urlParsed = url.parse(req.url, true);    // Getting the ticket (if it's defined in GET-params)    // If no ticket, then request will continue down the default    // middlewares.    const query = urlParsed.query;    if (query == null) {      next();      return;    }    const ticket = query.ticket;    if (ticket == null) {      next();      return;    }    const serviceUrl = Meteor.absoluteUrl(urlParsed.href.replace(/^\//g, '')).replace(/([&?])ticket=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');    const redirectUrl = serviceUrl;//.replace(/([&?])casToken=[^&]+[&]?/g, '$1').replace(/[?&]+$/g, '');    // get auth token    const credentialToken = query.casToken;    if (!credentialToken) {      end(res, redirectUrl);      return;    }    // validate ticket    casValidate(req, ticket, credentialToken, serviceUrl, () => {      end(res, redirectUrl);    });  } catch (err) {    console.log("account-cas: unexpected error : " + err.message);    end(res, redirectUrl);  }};const casValidate = (req, ticket, token, service, callback) => {  // get configuration  if (!Meteor.settings.cas/* || !Meteor.settings.cas.validate*/) {    throw new Error('accounts-cas: unable to get configuration.');  }  const cas = new CAS({    validate_url: Meteor.settings.cas.validateUrl,    service: service,    version: Meteor.settings.cas.casVersion  });  cas.validate(ticket, (err, status, userData) => {    if (err) {      console.log("accounts-cas: error when trying to validate " + err);      console.log(err);    } else {      if (status) {        console.log(`accounts-cas: user validated ${userData.id}          (${JSON.stringify(userData)})`);        _casCredentialTokens[token] = { id: userData.id };        _userData = userData;      } else {        console.log("accounts-cas: unable to validate " + ticket);      }    }    callback();  });  return;};/* * Register a server-side login handle. * It is call after Accounts.callLoginMethod() is call from client. */ Accounts.registerLoginHandler((options) => {  if (!options.cas)    return undefined;  if (!_hasCredential(options.cas.credentialToken)) {    throw new Meteor.Error(Accounts.LoginCancelledError.numericError,      'no matching login attempt found');  }  const result = _retrieveCredential(options.cas.credentialToken);  const attrs = Meteor.settings.cas.attributes || {};  // CAS keys  const fn = attrs.firstname || 'cas:givenName';  const ln = attrs.lastname || 'cas:sn';  const full = attrs.fullname;  const mail = attrs.mail || 'cas:mail'; // or 'email'  const uid = attrs.id || 'id';  if (attrs.debug) {    if (full) {      console.log(`CAS fields : id:"${uid}", fullname:"${full}", mail:"${mail}"`);    } else {      console.log(`CAS fields : id:"${uid}", firstname:"${fn}", lastname:"${ln}", mail:"${mail}"`);    }  }  const name = full ? _userData[full] : _userData[fn] + ' ' +  _userData[ln];  // https://docs.meteor.com/api/accounts.html#Meteor-users  options = {    // _id: Meteor.userId()    username: _userData[uid], // Unique name    emails: [      { address: _userData[mail], verified: true }    ],    createdAt: new Date(),    profile: {      // The profile is writable by the user by default.      name: name,      fullname : name,      email : _userData[mail]    },    active: true,    globalRoles: ['user']  };  if (attrs.debug) {    console.log(`CAS response : ${JSON.stringify(result)}`);  }  let user = Meteor.users.findOne({ 'username': options.username });  if (! user) {    if (attrs.debug) {      console.log(`Creating user account ${JSON.stringify(options)}`);    }    const userId = Accounts.insertUserDoc({}, options);    user = Meteor.users.findOne(userId);  }  if (attrs.debug) {    console.log(`Using user account ${JSON.stringify(user)}`);  }  return { userId: user._id };});const _hasCredential = (credentialToken) => {  return _.has(_casCredentialTokens, credentialToken);}/* * Retrieve token and delete it to avoid replaying it. */const _retrieveCredential = (credentialToken) => {  const result = _casCredentialTokens[credentialToken];  delete _casCredentialTokens[credentialToken];  return result;}const closePopup = (res) => {  if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {    return;  }  res.writeHead(200, {'Content-Type': 'text/html'});  const content = '<html><body><div id="popupCanBeClosed"></div></body></html>';  res.end(content, 'utf-8');}const redirect = (res, whereTo) => {  res.writeHead(302, {'Location': whereTo});  const content = '<html><head><meta http-equiv="refresh" content="0; url='+whereTo+'" /></head><body>Redirection to <a href='+whereTo+'>'+whereTo+'</a></body></html>';  res.end(content, 'utf-8');  return}const end = (res, whereTo) => {  if (Meteor.settings.cas && Meteor.settings.cas.popup == false) {    redirect(res, whereTo);  } else {    closePopup(res);  }}
 |