'use strict'; exports = module.exports = { start, stop, _MOCK_APP: null }; const addonConfigs = require('./addonconfigs.js'), assert = require('assert'), apps = require('./apps.js'), AuditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:ldap'), eventlog = require('./eventlog.js'), groups = require('./groups.js'), ldap = require('ldapjs'), mail = require('./mail.js'), safe = require('safetydance'), users = require('./users.js'), util = require('util'); let gServer = null; const NOOP = function () {}; // Will attach req.app if successful async function authenticateApp(req, res, next) { const sourceIp = req.connection.ldap.id.split(':')[0]; if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier')); // this is only used by the ldap test. the apps tests still uses proper docker if (constants.TEST && sourceIp === '127.0.0.1') { req.app = exports._MOCK_APP; return next(); } const [error, app] = await safe(apps.getByIpAddress(sourceIp)); if (error) return next(new ldap.OperationsError(error.message)); if (!app) return next(new ldap.OperationsError('Could not detect app source')); req.app = app; next(); } // Will attach req.user if successful async function userAuthInternal(appId, req, res, next) { // extract the common name which might have different attribute names const attributeName = Object.keys(req.dn.rdns[0].attrs)[0]; const commonName = req.dn.rdns[0].attrs[attributeName].value; if (!commonName) return next(new ldap.NoSuchObjectError('Missing CN')); let verifyFunc; if (attributeName === 'mail') { verifyFunc = users.verifyWithEmail; } else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check verifyFunc = users.verifyWithEmail; } else if (commonName.indexOf('uid-') === 0) { verifyFunc = users.verify; } else { verifyFunc = users.verifyWithUsername; } const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', appId || '', { skipTotpCheck: true })); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(error.message)); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(error.message)); if (error) return next(new ldap.OperationsError(error.message)); req.user = user; next(); } async function getUsersWithAccessToApp(req) { assert.strictEqual(typeof req.app, 'object'); const result = await users.list(); const allowedUsers = result.filter((user) => user.active && apps.canAccess(req.app, user)); // do not list inactive users return allowedUsers; } // helper function to deal with pagination function finalSend(results, req, res, next) { let min = 0; let max = results.length; let cookie = null; let pageSize = 0; // check if this is a paging request, if so get the cookie for session info req.controls.forEach(function (control) { if (control.type === ldap.PagedResultsControl.OID) { pageSize = control.value.size; cookie = control.value.cookie; } }); function sendPagedResults(start, end) { start = (start < min) ? min : start; end = (end > max || end < min) ? max : end; let i; for (i = start; i < end; i++) { res.send(results[i]); } return i; } if (cookie && Buffer.isBuffer(cookie)) { // we have pagination let first = min; if (cookie.length !== 0) { first = parseInt(cookie.toString(), 10); } const last = sendPagedResults(first, first + pageSize); let resultCookie; if (last < max) { resultCookie = Buffer.from(last.toString()); } else { resultCookie = Buffer.from(''); } res.controls.push(new ldap.PagedResultsControl({ value: { size: pageSize, // correctness not required here cookie: resultCookie } })); } else { // no pagination simply send all results.forEach(function (result) { res.send(result); }); } // all done res.end(); next(); } async function userSearch(req, res, next) { debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); const [error, result] = await safe(getUsersWithAccessToApp(req)); if (error) return next(new ldap.OperationsError(error.message)); const [groupsError, allGroups] = await safe(groups.listWithMembers()); if (groupsError) return next(new ldap.OperationsError(groupsError.message)); let results = []; // send user objects result.forEach(function (user) { // skip entries with empty username. Some apps like owncloud can't deal with this if (!user.username) return; const dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron'); const displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null const { firstName, lastName, middleName } = users.parseDisplayName(displayName); // https://datatracker.ietf.org/doc/html/rfc2798 const obj = { dn: dn.toString(), attributes: { objectclass: ['user', 'inetorgperson', 'person', 'organizationalperson', 'top' ], objectcategory: 'person', cn: displayName, uid: user.id, entryuuid: user.id, // to support OpenLDAP clients mail: user.email, mailAlternateAddress: user.fallbackEmail, displayname: displayName, givenName: firstName, sn: lastName, middleName: middleName, username: user.username, samaccountname: user.username, // to support ActiveDirectory clients memberof: allGroups.filter(function (g) { return g.userIds.indexOf(user.id) !== -1; }).map(function (g) { return `cn=${g.name},ou=groups,dc=cloudron`; }) } }; const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } }); finalSend(results, req, res, next); } async function groupSearch(req, res, next) { debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); const results = []; let [groupsListError, resultGroups] = await safe(groups.listWithMembers()); if (groupsListError) return next(new ldap.OperationsError(groupsListError.message)); if (req.app.accessRestriction && req.app.accessRestriction.groups) { resultGroups = resultGroups.filter(function (g) { return req.app.accessRestriction.groups.indexOf(g.id) !== -1; }); } resultGroups.forEach(function (group) { const dn = ldap.parseDN(`cn=${group.name},ou=groups,dc=cloudron`); const obj = { dn: dn.toString(), attributes: { objectclass: ['group'], cn: group.name, gidnumber: group.id, memberuid: group.userIds } }; // ensure all filter values are also lowercase const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } }); finalSend(results, req, res, next); } async function groupUsersCompare(req, res, next) { debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); const [error, result] = await safe(getUsersWithAccessToApp(req)); if (error) return next(new ldap.OperationsError(error.message)); // we only support memberuid here, if we add new group attributes later add them here if (req.attribute === 'memberuid') { const found = result.find(function (u) { return u.id === req.value; }); if (found) return res.end(true); } res.end(false); } async function groupAdminsCompare(req, res, next) { debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); const [error, result] = await safe(getUsersWithAccessToApp(req)); if (error) return next(new ldap.OperationsError(error.message)); // we only support memberuid here, if we add new group attributes later add them here if (req.attribute === 'memberuid') { const user = result.find(function (u) { return u.id === req.value; }); if (user && users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return res.end(true); } res.end(false); } async function mailboxSearch(req, res, next) { debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); // if cn is set OR filter is mail= we only search for one mailbox specifically let email, dn; if (req.dn.rdns[0].attrs.cn) { email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); dn = req.dn.toString(); } else if (req.filter instanceof ldap.EqualityFilter && req.filter.attribute === 'mail') { email = req.filter.value.toLowerCase(); dn = `cn=${email},${req.dn.toString()}`; } if (email) { const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(dn.toString())); const [error, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); if (error) return next(new ldap.OperationsError(error.message)); if (!mailbox) return next(new ldap.NoSuchObjectError(dn.toString())); if (!mailbox.active) return next(new ldap.NoSuchObjectError('Mailbox is not active')); const obj = { dn: dn.toString(), attributes: { objectclass: ['mailbox'], objectcategory: 'mailbox', cn: `${mailbox.name}@${mailbox.domain}`, uid: `${mailbox.name}@${mailbox.domain}`, mail: `${mailbox.name}@${mailbox.domain}`, storagequota: mailbox.storageQuota, messagesquota: mailbox.messagesQuota, } }; // ensure all filter values are also lowercase const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } } else { // new sogo and dovecot listing (doveadm -A) // TODO figure out how proper pagination here could work let [error, mailboxes] = await safe(mail.listAllMailboxes(1, 100000)); if (error) return next(new ldap.OperationsError(error.message)); mailboxes = mailboxes.filter(m => m.active); let results = []; for (const mailbox of mailboxes) { const dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); if (mailbox.ownerType === mail.OWNERTYPE_APP) continue; // cannot login with app mailbox anyway const [error, ownerObject] = await safe(mailbox.ownerType === mail.OWNERTYPE_USER ? users.get(mailbox.ownerId) : groups.get(mailbox.ownerId)); if (error || !ownerObject) continue; // skip mailboxes with unknown user const obj = { dn: dn.toString(), attributes: { objectclass: ['mailbox'], objectcategory: 'mailbox', displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name, cn: `${mailbox.name}@${mailbox.domain}`, uid: `${mailbox.name}@${mailbox.domain}`, mail: `${mailbox.name}@${mailbox.domain}`, storagequota: mailbox.storageQuota, messagesquota: mailbox.messagesQuota, } }; mailbox.aliases.forEach(function (a, idx) { obj.attributes['mail' + idx] = `${a.name}@${a.domain}`; }); // ensure all filter values are also lowercase const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } } finalSend(results, req, res, next); } } async function mailAliasSearch(req, res, next) { debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError('Missing CN')); const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError('Invalid CN')); const [error, alias] = await safe(mail.searchAlias(parts[0], parts[1])); if (error) return next(new ldap.OperationsError(error.message)); if (!alias) return next(new ldap.NoSuchObjectError('No such alias')); if (!alias.active) return next(new ldap.NoSuchObjectError('Mailbox is not active')); // there is no way to disable an alias. this is just here for completeness // https://wiki.debian.org/LDAP/MigrationTools/Examples // https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html // member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314 const obj = { dn: req.dn.toString(), attributes: { objectclass: ['nisMailAlias'], objectcategory: 'nisMailAlias', cn: `${parts[0]}@${alias.domain}`, // alias.name can contain wildcard character rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}` } }; // ensure all filter values are also lowercase const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } } async function mailingListSearch(req, res, next) { debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError('Missing CN')); let email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); let parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError('Invalid CN')); const name = parts[0], domain = parts[1]; const [error, result] = await safe(mail.resolveList(parts[0], parts[1])); if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError('No such list')); if (error) return next(new ldap.OperationsError(error.message)); const { resolvedMembers, list } = result; if (!list.active) return next(new ldap.NoSuchObjectError('List is not active')); // http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape // members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356) const obj = { dn: req.dn.toString(), attributes: { objectclass: ['mailGroup'], objectcategory: 'mailGroup', cn: `${name}@${domain}`, // fully qualified mail: `${name}@${domain}`, membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool! mgrpRFC822MailMember: resolvedMembers // fully qualified } }; // ensure all filter values are also lowercase const lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.message)); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } } // Will attach req.user if successful async function authenticateUser(req, res, next) { debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id); const appId = req.app.id; await userAuthInternal(appId, req, res, next); } async function authorizeUserForApp(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.app, 'object'); const canAccess = apps.canAccess(req.app, req.user); // we return no such object, to avoid leakage of a users existence if (!canAccess) return next(new ldap.NoSuchObjectError('Invalid user or insufficient previleges')); await eventlog.upsertLoginEvent(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, AuditSource.LDAP, { appId: req.app.id, userId: req.user.id, user: users.removePrivateFields(req.user) }); res.end(); } async function verifyMailboxPassword(mailbox, password) { assert.strictEqual(typeof mailbox, 'object'); assert.strictEqual(typeof password, 'string'); if (mailbox.ownerType === mail.OWNERTYPE_USER) { return await users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, { skipTotpCheck: true }); } else if (mailbox.ownerType === mail.OWNERTYPE_GROUP) { const userIds = await groups.getMembers(mailbox.ownerId); let verifiedUser = null; for (const userId of userIds) { const [error, result] = await safe(users.verify(userId, password, users.AP_MAIL /* identifier */, { skipTotpCheck: true })); if (error) continue; // try the next user verifiedUser = result; break; // found a matching validated user } if (!verifiedUser) throw new BoxError(BoxError.INVALID_CREDENTIALS); return verifiedUser; } else { throw new BoxError(BoxError.INVALID_CREDENTIALS); } } async function authenticateSftp(req, res, next) { debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError('Missing CN')); const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError('Invalid CN')); let [error, app] = await safe(apps.getByFqdn(parts[1])); if (error || !app) return next(new ldap.InvalidCredentialsError()); [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id, { skipTotpCheck: true })); if (error) return next(new ldap.InvalidCredentialsError(error.message)); debug('sftp auth: success'); res.end(); } async function userSearchSftp(req, res, next) { debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError()); const parts = req.filter.value.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError()); const username = parts[0]; const appFqdn = parts[1]; const [error, app] = await safe(apps.getByFqdn(appFqdn)); if (error) return next(new ldap.OperationsError(error.message)); // only allow apps which specify "ftp" support in the localstorage addon if (!safe.query(app.manifest.addons, 'localstorage.ftp.uid')) return next(new ldap.UnavailableError('Not supported')); if (typeof app.manifest.addons.localstorage.ftp.uid !== 'number') return next(new ldap.UnavailableError('Bad uid, must be a number')); const uidNumber = app.manifest.addons.localstorage.ftp.uid; const [userGetError, user] = await safe(users.getByUsername(username)); if (userGetError) return next(new ldap.OperationsError(userGetError.message)); if (!user) return next(new ldap.OperationsError('Invalid username')); if (!apps.isOperator(app, user)) return next(new ldap.InsufficientAccessRightsError('Not authorized')); const obj = { dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), attributes: { homeDirectory: app.storageVolumeId ? `/mnt/app-${app.id}` : `/mnt/appsdata/${app.id}/data`, // see also sftp.js objectclass: ['user'], objectcategory: 'person', cn: user.id, uid: `${username}@${appFqdn}`, // for bind after search uidNumber: uidNumber, // unix uid for ftp access gidNumber: uidNumber // unix gid for ftp access } }; finalSend([ obj ], req, res, next); } async function verifyAppMailboxPassword(serviceId, username, password) { assert.strictEqual(typeof serviceId, 'string'); assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); const pattern = serviceId === 'msa' ? 'MAIL_SMTP' : 'MAIL_IMAP'; const addonId = serviceId === 'msa' ? 'sendmail' : 'recvmail'; const appId = await addonConfigs.getAppIdByValue(addonId, `%${pattern}_PASSWORD`, password); // search by password because this is unique for each app if (!appId) throw new BoxError(BoxError.NOT_FOUND); const result = await addonConfigs.get(appId, addonId); if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) throw new BoxError(BoxError.INVALID_CREDENTIALS); } async function authenticateService(serviceId, dn, req, res, next) { debug(`authenticateService: ${req.dn.toString()} (from ${req.connection.ldap.id})`); if (!dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(dn.toString())); const email = dn.rdns[0].attrs.cn.value.toLowerCase(); const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(dn.toString())); const knownServices = [ 'msa', 'imap', 'pop3', 'sieve', 'sogo' ]; if (!knownServices.includes(serviceId)) return next(new ldap.OperationsError('Invalid DN. Unknown service')); const [error, domain] = await safe(mail.getDomain(parts[1])); if (error) return next(new ldap.OperationsError(error.message)); if (!domain) return next(new ldap.NoSuchObjectError(dn.toString())); const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3' || serviceId === 'sogo'; if (serviceNeedsMailbox && !domain.enabled) return next(new ldap.NoSuchObjectError(dn.toString())); const [getMailboxError, mailbox] = await safe(mail.getMailbox(parts[0], parts[1])); if (getMailboxError) return next(new ldap.OperationsError(getMailboxError.message)); if (serviceNeedsMailbox) { if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString())); if (serviceId === 'pop3' && !mailbox.enablePop3) return next(new ldap.OperationsError('POP3 is not enabled')); } const [appPasswordError] = await safe(verifyAppMailboxPassword(serviceId, email, req.credentials || '')); if (!appPasswordError) return res.end(); // validated as app if (appPasswordError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(appPasswordError.message)); if (appPasswordError.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(appPasswordError.message)); if (!mailbox || !mailbox.active) return next(new ldap.NoSuchObjectError(dn.toString())); // user auth requires active mailbox const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(verifyError.message)); if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(verifyError.message)); if (verifyError) return next(new ldap.OperationsError(verifyError.message)); eventlog.upsertLoginEvent(result.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, AuditSource.MAIL, { mailboxId: email, userId: result.id, user: users.removePrivateFields(result) }); res.end(); } async function authenticateMail(req, res, next) { if (!req.dn.rdns[1].attrs.ou) return next(new ldap.NoSuchObjectError()); await authenticateService(req.dn.rdns[1].attrs.ou.value.toLowerCase(), req.dn, req, res, next); } // https://ldapwiki.com/wiki/RootDSE / RFC 4512 - ldapsearch -x -h "${CLOUDRON_LDAP_SERVER}" -p "${CLOUDRON_LDAP_PORT}" -b "" -s base // ldapjs seems to call this handler for everything when search === '' async function maybeRootDSE(req, res, next) { debug(`maybeRootDSE: requested with scope:${req.scope} dn:${req.dn.toString()}`); if (req.scope !== 'base') return next(new ldap.NoSuchObjectError()); // per the spec, rootDSE search require base scope if (!req.dn || req.dn.toString() !== '') return next(new ldap.NoSuchObjectError()); res.send({ dn: '', attributes: { objectclass: [ 'RootDSE', 'top', 'OpenLDAProotDSE' ], supportedLDAPVersion: '3', vendorName: 'Cloudron LDAP', vendorVersion: '1.0.0' } }); res.end(); } async function start() { assert(gServer === null, 'Already started'); const logger = { trace: NOOP, debug: NOOP, info: debug, warn: debug, error: debug, fatal: debug }; gServer = ldap.createServer({ log: logger }); gServer.on('error', function (error) { debug('start: server error. %o', error); }); gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch); gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch); gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp); // http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search) gServer.bind('ou=mailboxes,dc=cloudron', async function (req, res, next) { // used for sogo only. this route happens only at sogo login time. after that it will use imap ldap route await authenticateService('sogo', req.dn, req, res, next); }); gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka gServer.bind('ou=imap,dc=cloudron', authenticateMail); // dovecot (IMAP auth) gServer.bind('ou=msa,dc=cloudron', authenticateMail); // haraka (MSA auth) gServer.bind('ou=sieve,dc=cloudron', authenticateMail); // dovecot (sieve auth) gServer.bind('ou=pop3,dc=cloudron', authenticateMail); // dovecot (pop3 auth) gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp gServer.search('ou=sftp,dc=cloudron', userSearchSftp); gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); // this is the bind for addons (after bind, they might search and authenticate) gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) { debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id res.end(); }); // this is the bind for apps (after bind, they might search and authenticate user) gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) { // TODO: validate password debug('application bind: %s', req.dn.toString()); res.end(); }); // directus looks for the "DN" of the bind user gServer.search('ou=apps,dc=cloudron', function(req, res, next) { const obj = { dn: req.dn.toString(), }; finalSend([obj], req, res, next); }); gServer.search('', maybeRootDSE); // when '', it seems the callback is called for everything else // just log that an attempt was made to unknown route, this helps a lot during app packaging gServer.use(function(req, res, next) { debug('not handled: dn %s, scope %s, filter %s (from %s)', req.dn ? req.dn.toString() : '-', req.scope, req.filter ? req.filter.toString() : '-', req.connection.ldap.id); return next(); }); await util.promisify(gServer.listen.bind(gServer))(constants.LDAP_PORT, '0.0.0.0'); } async function stop() { if (!gServer) return; await util.promisify(gServer.close.bind(gServer))(); gServer = null; }