2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2021-01-21 11:31:35 -08:00
start ,
2021-07-07 12:59:17 -07:00
stop ,
2021-11-22 21:10:36 +01:00
startExposed ,
stopExposed ,
2021-07-07 12:59:17 -07:00
_MOCK _APP : null
2015-07-20 00:09:47 -07:00
} ;
2021-08-19 21:39:27 -07:00
const addonConfigs = require ( './addonconfigs.js' ) ,
assert = require ( 'assert' ) ,
2016-02-18 16:04:53 +01:00
apps = require ( './apps.js' ) ,
2019-10-24 11:13:48 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-10-24 13:41:41 -07:00
constants = require ( './constants.js' ) ,
2015-07-20 00:09:47 -07:00
debug = require ( 'debug' ) ( 'box:ldap' ) ,
2021-11-22 21:10:36 +01:00
debugExposed = require ( 'debug' ) ( 'box:ldapExposed' ) ,
dns = require ( './dns.js' ) ,
domains = require ( './domains.js' ) ,
2016-04-30 23:16:37 -07:00
eventlog = require ( './eventlog.js' ) ,
2021-11-22 21:10:36 +01:00
fs = require ( 'fs' ) ,
2020-11-12 23:25:33 -08:00
groups = require ( './groups.js' ) ,
2016-05-29 17:25:23 -07:00
ldap = require ( 'ldapjs' ) ,
2018-01-22 20:35:08 +01:00
mail = require ( './mail.js' ) ,
2021-11-22 21:10:36 +01:00
reverseproxy = require ( './reverseproxy.js' ) ,
2019-03-22 15:42:16 -07:00
safe = require ( 'safetydance' ) ,
2021-11-22 21:10:36 +01:00
settings = require ( './settings.js' ) ,
2021-09-07 09:57:49 -07:00
users = require ( './users.js' ) ,
util = require ( 'util' ) ;
2015-07-20 00:09:47 -07:00
var gServer = null ;
2021-11-22 21:10:36 +01:00
var gExposedServer = null ;
2015-07-20 00:09:47 -07:00
2021-07-07 12:59:17 -07:00
const NOOP = function ( ) { } ;
2015-07-20 00:09:47 -07:00
2021-07-07 12:59:17 -07:00
const GROUP _USERS _DN = 'cn=users,ou=groups,dc=cloudron' ;
const GROUP _ADMINS _DN = 'cn=admins,ou=groups,dc=cloudron' ;
2015-08-12 15:31:44 +02:00
2018-09-03 15:38:50 +02:00
// Will attach req.app if successful
2021-08-20 09:19:44 -07:00
async function authenticateApp ( req , res , next ) {
2021-07-07 12:59:17 -07:00
const sourceIp = req . connection . ldap . id . split ( ':' ) [ 0 ] ;
2018-09-03 15:38:50 +02:00
if ( sourceIp . split ( '.' ) . length !== 4 ) return next ( new ldap . InsufficientAccessRightsError ( 'Missing source identifier' ) ) ;
2016-02-18 16:04:53 +01:00
2021-07-07 12:59:17 -07:00
// 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 ( ) ;
}
2021-08-20 09:19:44 -07:00
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' ) ) ;
2016-06-17 10:08:41 -05:00
2021-08-20 09:19:44 -07:00
req . app = app ;
2016-06-17 10:08:41 -05:00
2021-08-20 09:19:44 -07:00
next ( ) ;
2016-02-18 16:04:53 +01:00
}
2021-08-20 09:19:44 -07:00
async function getUsersWithAccessToApp ( req ) {
2018-09-03 15:38:50 +02:00
assert . strictEqual ( typeof req . app , 'object' ) ;
2021-07-15 09:50:11 -07:00
2021-08-20 09:19:44 -07:00
const result = await users . list ( ) ;
2021-09-21 10:00:47 -07:00
const allowedUsers = result . filter ( ( user ) => apps . canAccess ( req . app , user ) ) ;
2021-08-20 09:19:44 -07:00
return allowedUsers ;
2016-05-12 13:36:53 -07:00
}
2015-07-20 00:09:47 -07:00
2017-10-27 01:25:07 +02:00
// helper function to deal with pagination
function finalSend ( results , req , res , next ) {
2021-08-20 09:19:44 -07:00
let min = 0 ;
let max = results . length ;
let cookie = null ;
let pageSize = 0 ;
2017-10-27 01:25:07 +02:00
// 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 ;
2021-08-20 09:19:44 -07:00
let i ;
2017-10-27 01:25:07 +02:00
for ( i = start ; i < end ; i ++ ) {
res . send ( results [ i ] ) ;
}
return i ;
}
if ( cookie && Buffer . isBuffer ( cookie ) ) {
// we have pagination
var first = min ;
if ( cookie . length !== 0 ) {
first = parseInt ( cookie . toString ( ) , 10 ) ;
}
var last = sendPagedResults ( first , first + pageSize ) ;
var resultCookie ;
if ( last < max ) {
2019-03-21 20:06:14 -07:00
resultCookie = Buffer . from ( last . toString ( ) ) ;
2017-10-27 01:25:07 +02:00
} else {
2019-03-21 20:06:14 -07:00
resultCookie = Buffer . from ( '' ) ;
2017-10-27 01:25:07 +02:00
}
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 ( ) ;
}
2021-08-20 09:19:44 -07:00
async function userSearch ( req , res , next ) {
2017-03-13 11:09:12 +01:00
debug ( 'user search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2021-08-20 09:19:44 -07:00
const [ error , result ] = await safe ( getUsersWithAccessToApp ( req ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2017-10-27 01:25:07 +02:00
2021-08-20 09:19:44 -07:00
let results = [ ] ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
// 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 ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
const dn = ldap . parseDN ( 'cn=' + user . id + ',ou=users,dc=cloudron' ) ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
const memberof = [ GROUP _USERS _DN ] ;
if ( users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ) memberof . push ( GROUP _ADMINS _DN ) ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
const displayName = user . displayName || user . username || '' ; // displayName can be empty and username can be null
const nameParts = displayName . split ( ' ' ) ;
const firstName = nameParts [ 0 ] ;
const lastName = nameParts . length > 1 ? nameParts [ nameParts . length - 1 ] : '' ; // choose last part, if it exists
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
const obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'user' , 'inetorgperson' , 'person' ] ,
objectcategory : 'person' ,
cn : user . id ,
uid : user . id ,
entryuuid : user . id , // to support OpenLDAP clients
mail : user . email ,
mailAlternateAddress : user . fallbackEmail ,
displayname : displayName ,
givenName : firstName ,
username : user . username ,
samaccountname : user . username , // to support ActiveDirectory clients
memberof : memberof
}
} ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
// http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString
// which is required to have atleast one character if present
if ( lastName . length !== 0 ) obj . attributes . sn = lastName ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
// 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 . toString ( ) ) ) ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
}
2017-03-13 11:09:12 +01:00
} ) ;
2021-08-20 09:19:44 -07:00
finalSend ( results , req , res , next ) ;
2017-03-13 11:09:12 +01:00
}
2021-08-20 09:19:44 -07:00
async function groupSearch ( req , res , next ) {
2016-05-16 14:14:58 -07:00
debug ( 'group search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2016-05-12 13:36:53 -07:00
2021-08-20 09:19:44 -07:00
const [ error , result ] = await safe ( getUsersWithAccessToApp ( req ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2016-05-12 13:36:53 -07:00
2021-08-20 09:19:44 -07:00
const results = [ ] ;
2017-10-27 01:25:07 +02:00
2021-08-20 09:19:44 -07:00
const groups = [ {
name : 'users' ,
admin : false
} , {
name : 'admins' ,
admin : true
} ] ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
groups . forEach ( function ( group ) {
const dn = ldap . parseDN ( 'cn=' + group . name + ',ou=groups,dc=cloudron' ) ;
const members = group . admin ? result . filter ( function ( user ) { return users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ; } ) : result ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
const obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'group' ] ,
cn : group . name ,
memberuid : members . map ( function ( entry ) { return entry . id ; } )
2017-03-13 11:09:12 +01:00
}
2021-08-20 09:19:44 -07:00
} ;
2017-03-13 11:09:12 +01:00
2021-08-20 09:19:44 -07:00
// 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 . toString ( ) ) ) ;
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
}
2015-07-20 00:09:47 -07:00
} ) ;
2021-08-20 09:19:44 -07:00
finalSend ( results , req , res , next ) ;
2016-05-12 13:36:53 -07:00
}
2021-08-20 09:19:44 -07:00
async function groupUsersCompare ( req , res , next ) {
2017-10-24 01:35:35 +02:00
debug ( 'group users compare: dn %s, attribute %s, value %s (from %s)' , req . dn . toString ( ) , req . attribute , req . value , req . connection . ldap . id ) ;
2021-08-20 09:19:44 -07:00
const [ error , result ] = await safe ( getUsersWithAccessToApp ( req ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2017-10-24 01:35:35 +02:00
2021-08-20 09:19:44 -07:00
// 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 ) ;
}
2017-10-24 01:35:35 +02:00
2021-08-20 09:19:44 -07:00
res . end ( false ) ;
2017-10-24 01:35:35 +02:00
}
2021-08-20 09:19:44 -07:00
async function groupAdminsCompare ( req , res , next ) {
2017-10-24 01:35:35 +02:00
debug ( 'group admins compare: dn %s, attribute %s, value %s (from %s)' , req . dn . toString ( ) , req . attribute , req . value , req . connection . ldap . id ) ;
2021-08-20 09:19:44 -07:00
const [ error , result ] = await safe ( getUsersWithAccessToApp ( req ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2017-10-24 01:35:35 +02:00
2021-08-20 09:19:44 -07:00
// we only support memberuid here, if we add new group attributes later add them here
if ( req . attribute === 'memberuid' ) {
var user = result . find ( function ( u ) { return u . id === req . value ; } ) ;
if ( user && users . compareRoles ( user . role , users . ROLE _ADMIN ) >= 0 ) return res . end ( true ) ;
}
2017-10-24 01:35:35 +02:00
2021-08-20 09:19:44 -07:00
res . end ( false ) ;
2017-10-24 01:35:35 +02:00
}
2021-08-17 15:45:57 -07:00
async function mailboxSearch ( req , res , next ) {
2016-09-26 10:18:58 -07:00
debug ( 'mailbox search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2016-05-29 18:24:54 -07:00
2018-05-03 18:05:32 +02:00
// if cn is set we only search for one mailbox specifically
if ( req . dn . rdns [ 0 ] . attrs . cn ) {
2021-08-17 15:45:57 -07:00
const email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
const parts = email . split ( '@' ) ;
2018-05-03 18:05:32 +02:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2017-10-27 01:25:07 +02:00
2021-08-17 15:45:57 -07:00
const [ error , mailbox ] = await safe ( mail . getMailbox ( parts [ 0 ] , parts [ 1 ] ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
if ( ! mailbox ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
if ( ! mailbox . active ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
const obj = {
dn : req . dn . toString ( ) ,
attributes : {
objectclass : [ 'mailbox' ] ,
objectcategory : 'mailbox' ,
cn : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
uid : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
mail : ` ${ mailbox . name } @ ${ mailbox . 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 . toString ( ) ) ) ;
if ( lowerCaseFilter . matches ( obj . attributes ) ) {
finalSend ( [ obj ] , req , res , next ) ;
} else {
res . end ( ) ;
}
2020-03-06 13:05:31 -08:00
} else { // new sogo
2021-08-17 15:45:57 -07:00
let [ error , mailboxes ] = await safe ( mail . listAllMailboxes ( 1 , 1000 ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2018-05-03 18:05:32 +02:00
2021-08-17 15:45:57 -07:00
mailboxes = mailboxes . filter ( m => m . active ) ;
2021-04-14 22:37:01 -07:00
2021-08-17 15:45:57 -07:00
let results = [ ] ;
2018-05-03 18:05:32 +02:00
2021-08-17 15:45:57 -07:00
for ( const mailbox of mailboxes ) {
const dn = ldap . parseDN ( ` cn= ${ mailbox . name } @ ${ mailbox . domain } ,ou=mailboxes,dc=cloudron ` ) ;
2021-07-15 09:50:11 -07:00
2021-08-17 15:45:57 -07:00
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
2021-07-15 09:50:11 -07:00
2021-08-17 15:45:57 -07:00
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 } `
}
} ;
2020-03-05 22:40:25 -08:00
2021-08-17 15:45:57 -07:00
mailbox . aliases . forEach ( function ( a , idx ) {
obj . attributes [ 'mail' + idx ] = ` ${ a . name } @ ${ a . domain } ` ;
} ) ;
2020-03-05 22:40:25 -08:00
2021-08-17 15:45:57 -07:00
// 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 . toString ( ) ) ) ;
2021-07-15 09:50:11 -07:00
2021-08-17 15:45:57 -07:00
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
2021-07-15 09:50:11 -07:00
}
2021-08-17 15:45:57 -07:00
}
2021-07-15 09:50:11 -07:00
2021-08-17 15:45:57 -07:00
finalSend ( results , req , res , next ) ;
2018-05-03 18:05:32 +02:00
}
2016-09-25 18:59:11 -07:00
}
2021-08-17 15:45:57 -07:00
async function mailAliasSearch ( req , res , next ) {
2016-09-25 18:59:11 -07:00
debug ( 'mail alias get: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2016-09-27 11:45:49 -07:00
if ( ! req . dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2017-10-27 01:25:07 +02:00
2021-08-17 15:45:57 -07:00
const email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
const parts = email . split ( '@' ) ;
2018-01-18 18:14:31 -08:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-08-17 15:45:57 -07:00
const [ error , alias ] = await safe ( mail . getAlias ( parts [ 0 ] , parts [ 1 ] ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
if ( ! alias ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
if ( ! alias . active ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ; // 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 : ` ${ alias . name } @ ${ alias . domain } ` ,
rfc822MailMember : ` ${ alias . aliasName } @ ${ alias . aliasDomain } `
}
} ;
2016-09-25 18:59:11 -07:00
2021-08-17 15:45:57 -07:00
// 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 . toString ( ) ) ) ;
2016-09-25 18:59:11 -07:00
2021-08-17 15:45:57 -07:00
if ( lowerCaseFilter . matches ( obj . attributes ) ) {
finalSend ( [ obj ] , req , res , next ) ;
} else {
res . end ( ) ;
}
2016-09-25 18:59:11 -07:00
}
2021-08-17 15:45:57 -07:00
async function mailingListSearch ( req , res , next ) {
2016-09-27 12:20:20 -07:00
debug ( 'mailing list get: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2016-09-25 18:59:11 -07:00
2016-09-27 12:20:20 -07:00
if ( ! req . dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2017-10-27 01:25:07 +02:00
2019-11-06 16:45:44 -08:00
let email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
let parts = email . split ( '@' ) ;
2018-01-18 18:14:31 -08:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2019-11-06 16:45:44 -08:00
const name = parts [ 0 ] , domain = parts [ 1 ] ;
2018-01-18 18:14:31 -08:00
2021-08-17 15:45:57 -07:00
const [ error , result ] = await safe ( mail . resolveList ( parts [ 0 ] , parts [ 1 ] ) ) ;
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
const { resolvedMembers , list } = result ;
if ( ! list . active ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
// 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
}
} ;
2021-04-14 22:37:01 -07:00
2021-08-17 15:45:57 -07:00
// 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 . toString ( ) ) ) ;
2016-05-29 18:24:54 -07:00
2021-08-17 15:45:57 -07:00
if ( lowerCaseFilter . matches ( obj . attributes ) ) {
finalSend ( [ obj ] , req , res , next ) ;
} else {
res . end ( ) ;
}
2016-05-29 18:24:54 -07:00
}
2018-09-03 15:38:50 +02:00
// Will attach req.user if successful
2021-07-15 09:50:11 -07:00
async function authenticateUser ( req , res , next ) {
2016-05-16 14:14:58 -07:00
debug ( 'user bind: %s (from %s)' , req . dn . toString ( ) , req . connection . ldap . id ) ;
2016-05-12 13:36:53 -07:00
// extract the common name which might have different attribute names
2021-07-15 09:50:11 -07:00
const attributeName = Object . keys ( req . dn . rdns [ 0 ] . attrs ) [ 0 ] ;
const commonName = req . dn . rdns [ 0 ] . attrs [ attributeName ] . value ;
2016-05-12 13:36:53 -07:00
if ( ! commonName ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-07-15 09:50:11 -07:00
let verifyFunc ;
2016-05-16 12:21:15 -07:00
if ( attributeName === 'mail' ) {
2021-07-15 09:50:11 -07:00
verifyFunc = users . verifyWithEmail ;
2016-05-16 12:21:15 -07:00
} else if ( commonName . indexOf ( '@' ) !== - 1 ) { // if mail is specified, enforce mail check
2021-07-15 09:50:11 -07:00
verifyFunc = users . verifyWithEmail ;
2016-05-12 13:36:53 -07:00
} else if ( commonName . indexOf ( 'uid-' ) === 0 ) {
2021-07-15 09:50:11 -07:00
verifyFunc = users . verify ;
2016-05-12 13:36:53 -07:00
} else {
2021-07-15 09:50:11 -07:00
verifyFunc = users . verifyWithUsername ;
2016-05-12 13:36:53 -07:00
}
2021-07-15 09:50:11 -07:00
const [ error , user ] = await safe ( verifyFunc ( commonName , req . credentials || '' , req . app . id ) ) ;
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
if ( error && error . reason === BoxError . INVALID _CREDENTIALS ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2016-05-12 13:36:53 -07:00
2021-07-15 09:50:11 -07:00
req . user = user ;
2016-05-12 13:36:53 -07:00
2021-07-15 09:50:11 -07:00
next ( ) ;
2016-05-29 17:16:52 -07:00
}
2021-08-20 09:19:44 -07:00
async function authorizeUserForApp ( req , res , next ) {
2018-09-03 15:38:50 +02:00
assert . strictEqual ( typeof req . user , 'object' ) ;
assert . strictEqual ( typeof req . app , 'object' ) ;
2016-05-29 17:16:52 -07:00
2021-09-21 10:00:47 -07:00
const canAccess = apps . canAccess ( req . app , req . user ) ;
2021-08-20 09:19:44 -07:00
// we return no such object, to avoid leakage of a users existence
2021-09-21 10:00:47 -07:00
if ( ! canAccess ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2016-05-12 13:36:53 -07:00
2021-08-20 09:19:44 -07:00
await eventlog . upsertLoginEvent ( eventlog . ACTION _USER _LOGIN , { authType : 'ldap' , appId : req . app . id } , { userId : req . user . id , user : users . removePrivateFields ( req . user ) } ) ;
2016-05-12 13:36:53 -07:00
2021-08-20 09:19:44 -07:00
res . end ( ) ;
2016-05-12 13:36:53 -07:00
}
2021-07-15 09:50:11 -07:00
async function verifyMailboxPassword ( mailbox , password ) {
2020-11-12 23:25:33 -08:00
assert . strictEqual ( typeof mailbox , 'object' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2021-07-15 09:50:11 -07:00
if ( mailbox . ownerType === mail . OWNERTYPE _USER ) return await users . verify ( mailbox . ownerId , password , users . AP _MAIL /* identifier */ ) ;
2020-11-12 23:25:33 -08:00
2021-07-15 09:50:11 -07:00
const userIds = await groups . getMembers ( mailbox . ownerId ) ;
2020-11-12 23:25:33 -08:00
2021-06-28 15:15:28 -07:00
let verifiedUser = null ;
2021-07-15 09:50:11 -07:00
for ( const userId of userIds ) {
const [ error , result ] = await safe ( users . verify ( userId , password , users . AP _MAIL /* identifier */ ) ) ;
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 ;
2020-11-12 23:25:33 -08:00
}
2021-08-20 09:19:44 -07:00
async function authenticateSftp ( req , res , next ) {
2019-04-04 20:46:01 -07:00
debug ( 'sftp auth: %s (from %s)' , req . dn . toString ( ) , req . connection . ldap . id ) ;
2019-03-19 11:59:51 -07:00
2019-03-18 21:15:50 -07:00
if ( ! req . dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-07-15 09:50:11 -07:00
const email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
const parts = email . split ( '@' ) ;
2019-03-18 21:15:50 -07:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-08-20 09:19:44 -07:00
let [ error , app ] = await safe ( apps . getByFqdn ( parts [ 1 ] ) ) ;
if ( error || ! app ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
[ error ] = await safe ( users . verifyWithUsername ( parts [ 0 ] , req . credentials , app . id ) ) ;
if ( error ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
debug ( 'sftp auth: success' ) ;
2020-03-26 21:50:25 -07:00
2021-08-20 09:19:44 -07:00
res . end ( ) ;
2019-03-18 21:15:50 -07:00
}
2021-08-20 09:19:44 -07:00
async function userSearchSftp ( req , res , next ) {
2019-04-04 20:46:01 -07:00
debug ( 'sftp user search: dn %s, scope %s, filter %s (from %s)' , req . dn . toString ( ) , req . scope , req . filter . toString ( ) , req . connection . ldap . id ) ;
2019-03-19 11:59:51 -07:00
2019-03-18 21:15:50 -07:00
if ( req . filter . attribute !== 'username' || ! req . filter . value ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-08-20 09:19:44 -07:00
const parts = req . filter . value . split ( '@' ) ;
2019-03-18 21:15:50 -07:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2021-08-20 09:19:44 -07:00
const username = parts [ 0 ] ;
const appFqdn = parts [ 1 ] ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
const [ error , app ] = await safe ( apps . getByFqdn ( appFqdn ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
// 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' ) ) ;
2019-04-04 22:38:40 -07:00
2021-08-20 09:19:44 -07:00
const uidNumber = app . manifest . addons . localstorage . ftp . uid ;
2019-03-19 21:17:23 -07:00
2021-08-20 09:19:44 -07:00
const [ userGetError , user ] = await safe ( users . getByUsername ( username ) ) ;
if ( userGetError ) return next ( new ldap . OperationsError ( userGetError . toString ( ) ) ) ;
if ( ! user ) return next ( new ldap . OperationsError ( 'Invalid username' ) ) ;
2019-03-18 21:15:50 -07:00
2021-09-21 22:42:32 -07:00
if ( ! apps . isOperator ( app , user ) ) return next ( new ldap . InsufficientAccessRightsError ( 'Not authorized' ) ) ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
const obj = {
dn : ldap . parseDN ( ` cn= ${ username } @ ${ appFqdn } ,ou=sftp,dc=cloudron ` ) . toString ( ) ,
attributes : {
homeDirectory : app . dataDir ? ` /mnt/ ${ app . id } ` : ` /mnt/appsdata/ ${ app . id } /data ` ,
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
}
} ;
2019-03-18 21:15:50 -07:00
2021-08-20 09:19:44 -07:00
finalSend ( [ obj ] , req , res , next ) ;
2019-03-18 21:15:50 -07:00
}
2021-09-20 19:30:00 -07:00
async function verifyAppMailboxPassword ( serviceId , username , password ) {
assert . strictEqual ( typeof serviceId , 'string' ) ;
2020-12-03 12:14:04 -08:00
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
2021-09-20 19:30:00 -07:00
const pattern = serviceId === 'msa' ? 'MAIL_SMTP' : 'MAIL_IMAP' ;
2021-10-08 09:59:44 -07:00
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
2021-08-19 21:39:27 -07:00
if ( ! appId ) throw new BoxError ( BoxError . NOT _FOUND ) ;
2020-12-03 12:14:04 -08:00
2021-10-08 09:59:44 -07:00
const result = await addonConfigs . get ( appId , addonId ) ;
2020-12-03 12:14:04 -08:00
2021-08-19 21:39:27 -07:00
if ( ! result . some ( r => r . name . endsWith ( ` ${ pattern } _USERNAME ` ) && r . value === username ) ) throw new BoxError ( BoxError . INVALID _CREDENTIALS ) ;
2020-12-03 12:14:04 -08:00
}
2021-11-01 17:17:47 -07:00
async function authenticateService ( serviceId , dn , req , res , next ) {
debug ( ` authenticateService: ${ req . dn . toString ( ) } (from ${ req . connection . ldap . id } ) ` ) ;
2018-02-08 18:49:27 -08:00
2021-11-01 17:17:47 -07:00
if ( ! dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ;
2020-12-03 13:35:50 -08:00
2021-11-01 17:17:47 -07:00
const email = dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
2020-12-03 13:35:50 -08:00
const parts = email . split ( '@' ) ;
2021-11-01 17:17:47 -07:00
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ;
2016-09-26 11:50:32 -07:00
2021-11-01 17:17:47 -07:00
const knownServices = [ 'msa' , 'imap' , 'pop3' , 'sieve' , 'sogo' ] ;
2021-10-08 10:15:48 -07:00
if ( ! knownServices . includes ( serviceId ) ) return next ( new ldap . OperationsError ( 'Invalid DN. Unknown service' ) ) ;
2018-12-06 21:08:19 -08:00
2021-08-17 15:45:57 -07:00
const [ error , domain ] = await safe ( mail . getDomain ( parts [ 1 ] ) ) ;
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2021-11-01 17:17:47 -07:00
if ( ! domain ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ;
2018-01-22 20:35:08 +01:00
2021-11-01 17:17:47 -07:00
const serviceNeedsMailbox = serviceId === 'imap' || serviceId === 'sieve' || serviceId === 'pop3' || serviceId === 'sogo' ;
if ( serviceNeedsMailbox && ! domain . enabled ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ;
2018-01-22 20:35:08 +01:00
2021-10-03 23:59:06 -07:00
const [ getMailboxError , mailbox ] = await safe ( mail . getMailbox ( parts [ 0 ] , parts [ 1 ] ) ) ;
if ( getMailboxError ) return next ( new ldap . OperationsError ( getMailboxError . message ) ) ;
2021-11-01 19:53:38 -07:00
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' ) ) ;
2021-10-03 23:59:06 -07:00
}
2018-01-22 20:35:08 +01:00
2021-11-01 19:53:38 -07:00
const [ appPasswordError ] = await safe ( verifyAppMailboxPassword ( serviceId , email , req . credentials || '' ) ) ;
if ( ! appPasswordError ) return res . end ( ) ; // validated as app
2018-01-29 19:29:04 +01:00
2021-11-01 19:53:38 -07:00
if ( appPasswordError . reason === BoxError . INVALID _CREDENTIALS ) return next ( new ldap . InvalidCredentialsError ( dn . toString ( ) ) ) ;
if ( appPasswordError . reason !== BoxError . NOT _FOUND ) return next ( new ldap . OperationsError ( appPasswordError . message ) ) ;
2021-04-14 22:37:01 -07:00
2021-11-01 19:53:38 -07:00
if ( ! mailbox || ! mailbox . active ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ; // user auth requires active mailbox
2021-08-17 15:45:57 -07:00
const [ verifyError , result ] = await safe ( verifyMailboxPassword ( mailbox , req . credentials || '' ) ) ;
2021-11-01 17:17:47 -07:00
if ( verifyError && verifyError . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( dn . toString ( ) ) ) ;
if ( verifyError && verifyError . reason === BoxError . INVALID _CREDENTIALS ) return next ( new ldap . InvalidCredentialsError ( dn . toString ( ) ) ) ;
2021-08-17 15:45:57 -07:00
if ( verifyError ) return next ( new ldap . OperationsError ( verifyError . message ) ) ;
2018-01-22 20:35:08 +01:00
2021-08-17 15:45:57 -07:00
eventlog . upsertLoginEvent ( eventlog . ACTION _USER _LOGIN , { authType : 'ldap' , mailboxId : email } , { userId : result . id , user : users . removePrivateFields ( result ) } ) ;
2021-06-01 09:35:20 -07:00
2021-08-17 15:45:57 -07:00
res . end ( ) ;
2016-05-29 17:25:23 -07:00
}
2021-11-01 17:17:47 -07:00
async function authenticateMail ( req , res , next ) {
if ( ! req . dn . rdns [ 1 ] . attrs . ou ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
await authenticateService ( req . dn . rdns [ 1 ] . attrs . ou . value . toLowerCase ( ) , req . dn , req , res , next ) ;
}
2021-09-07 09:57:49 -07:00
async function start ( ) {
2021-08-20 09:19:44 -07:00
const logger = {
2016-09-25 16:11:54 -07:00
trace : NOOP ,
debug : NOOP ,
info : debug ,
warn : debug ,
2020-08-02 11:43:18 -07:00
error : debug ,
fatal : debug
2016-09-25 16:11:54 -07:00
} ;
gServer = ldap . createServer ( { log : logger } ) ;
2016-05-12 13:36:53 -07:00
2019-04-25 13:10:52 +02:00
gServer . on ( 'error' , function ( error ) {
2020-08-02 11:43:18 -07:00
debug ( 'start: server error ' , error ) ;
2019-04-25 13:10:52 +02:00
} ) ;
2018-09-03 15:38:50 +02:00
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 ) ;
2015-07-20 00:09:47 -07:00
2016-09-25 18:59:11 -07:00
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
2020-11-12 23:25:33 -08:00
gServer . search ( 'ou=mailboxes,dc=cloudron' , mailboxSearch ) ; // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
2021-11-01 17:17:47 -07:00
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 ) ;
} ) ;
2018-12-16 18:04:30 -08:00
gServer . search ( 'ou=mailaliases,dc=cloudron' , mailAliasSearch ) ; // haraka
gServer . search ( 'ou=mailinglists,dc=cloudron' , mailingListSearch ) ; // haraka
2016-09-25 18:59:11 -07:00
2021-09-20 19:30:00 -07:00
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)
2021-10-08 10:15:48 -07:00
gServer . bind ( 'ou=pop3,dc=cloudron' , authenticateMail ) ; // dovecot (pop3 auth)
2016-05-29 17:25:23 -07:00
2019-04-04 20:46:01 -07:00
gServer . bind ( 'ou=sftp,dc=cloudron' , authenticateSftp ) ; // sftp
2021-09-21 22:42:32 -07:00
gServer . search ( 'ou=sftp,dc=cloudron' , userSearchSftp ) ;
2019-03-18 21:15:50 -07:00
2018-09-03 15:38:50 +02:00
gServer . compare ( 'cn=users,ou=groups,dc=cloudron' , authenticateApp , groupUsersCompare ) ;
gServer . compare ( 'cn=admins,ou=groups,dc=cloudron' , authenticateApp , groupAdminsCompare ) ;
2017-10-24 01:35:35 +02:00
2016-05-12 13:20:57 -07:00
// this is the bind for addons (after bind, they might search and authenticate)
2017-11-15 18:07:10 -08:00
gServer . bind ( 'ou=addons,dc=cloudron' , function ( req , res /*, next */ ) {
2016-05-12 13:20:57 -07:00
debug ( 'addons bind: %s' , req . dn . toString ( ) ) ; // note: cn can be email or id
2016-05-11 14:26:34 -07:00
res . end ( ) ;
} ) ;
2016-05-12 13:20:57 -07:00
// this is the bind for apps (after bind, they might search and authenticate user)
2017-11-15 18:07:10 -08:00
gServer . bind ( 'ou=apps,dc=cloudron' , function ( req , res /*, next */ ) {
2015-09-25 21:17:48 -07:00
// TODO: validate password
2016-03-04 17:50:48 -08:00
debug ( 'application bind: %s' , req . dn . toString ( ) ) ;
2015-09-25 21:17:48 -07:00
res . end ( ) ;
} ) ;
2021-02-13 18:56:36 +01:00
// 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 ( ) ;
} ) ;
2021-09-07 09:57:49 -07:00
await util . promisify ( gServer . listen . bind ( gServer ) ) ( constants . LDAP _PORT , '0.0.0.0' ) ;
2015-07-20 00:09:47 -07:00
}
2015-09-14 10:59:05 -07:00
2021-09-07 09:57:49 -07:00
async function stop ( ) {
2016-09-15 11:53:28 -07:00
if ( gServer ) gServer . close ( ) ;
2015-09-14 10:59:05 -07:00
}
2021-11-22 21:10:36 +01:00
// FIXME this needs to be restarted if settings changes or dashboard cert got renewed
async function startExposed ( ) {
const logger = {
trace : NOOP ,
debug : NOOP ,
info : debugExposed ,
warn : debugExposed ,
error : debugExposed ,
fatal : debugExposed
} ;
const domainObject = await domains . get ( settings . dashboardDomain ( ) ) ;
const dashboardFqdn = dns . fqdn ( constants . DASHBOARD _LOCATION , domainObject ) ;
const bundle = await reverseproxy . getCertificatePath ( dashboardFqdn , domainObject . domain ) ;
gExposedServer = ldap . createServer ( {
certificate : fs . readFileSync ( bundle . certFilePath , 'utf8' ) ,
key : fs . readFileSync ( bundle . keyFilePath , 'utf8' ) ,
log : logger
} ) ;
gExposedServer . on ( 'error' , function ( error ) {
debugExposed ( 'start: server error ' , error ) ;
} ) ;
gExposedServer . search ( 'ou=users,dc=cloudron' , authenticateApp , userSearch ) ;
gExposedServer . search ( 'ou=groups,dc=cloudron' , authenticateApp , groupSearch ) ;
gExposedServer . bind ( 'ou=users,dc=cloudron' , authenticateApp , authenticateUser , authorizeUserForApp ) ;
gExposedServer . compare ( 'cn=users,ou=groups,dc=cloudron' , authenticateApp , groupUsersCompare ) ;
gExposedServer . compare ( 'cn=admins,ou=groups,dc=cloudron' , authenticateApp , groupAdminsCompare ) ;
// just log that an attempt was made to unknown route, this helps a lot during app packaging
gExposedServer . use ( function ( req , res , next ) {
debugExposed ( '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 ( gExposedServer . listen . bind ( gExposedServer ) ) ( constants . LDAPS _PORT , '0.0.0.0' ) ;
}
async function stopExposed ( ) {
if ( gExposedServer ) gExposedServer . close ( ) ;
}