2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2021-01-21 11:31:35 -08:00
start ,
stop
2015-07-20 00:09:47 -07:00
} ;
2021-01-21 11:31:35 -08:00
const assert = require ( 'assert' ) ,
2017-03-26 19:14:50 -07:00
appdb = require ( './appdb.js' ) ,
2016-02-18 16:04:53 +01:00
apps = require ( './apps.js' ) ,
2017-03-13 11:01:11 +01:00
async = require ( 'async' ) ,
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' ) ,
2016-04-30 23:16:37 -07:00
eventlog = require ( './eventlog.js' ) ,
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' ) ,
2016-09-21 15:34:58 -07:00
mailboxdb = require ( './mailboxdb.js' ) ,
2019-03-18 21:15:50 -07:00
path = require ( 'path' ) ,
2019-03-22 15:42:16 -07:00
safe = require ( 'safetydance' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2019-10-24 14:40:26 -07:00
users = require ( './users.js' ) ;
2015-07-20 00:09:47 -07:00
var gServer = null ;
var NOOP = function ( ) { } ;
2015-08-12 15:31:44 +02:00
var GROUP _USERS _DN = 'cn=users,ou=groups,dc=cloudron' ;
2015-08-18 16:35:52 -07:00
var 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
function authenticateApp ( req , res , next ) {
2016-02-18 16:40:30 +01:00
var 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
apps . getByIpAddress ( sourceIp , function ( error , app ) {
2018-09-03 15:38:50 +02:00
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
2018-09-03 15:38:50 +02:00
req . app = app ;
2016-06-17 10:08:41 -05:00
2018-09-03 15:38:50 +02:00
next ( ) ;
2016-02-18 16:04:53 +01:00
} ) ;
}
2017-03-13 11:09:12 +01:00
function getUsersWithAccessToApp ( req , callback ) {
2018-09-03 15:38:50 +02:00
assert . strictEqual ( typeof req . app , 'object' ) ;
2017-03-13 11:10:08 +01:00
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-14 16:39:20 +01:00
users . getAll ( function ( error , result ) {
2018-09-03 15:38:50 +02:00
if ( error ) return callback ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2017-03-13 11:01:11 +01:00
2018-09-03 15:38:50 +02:00
async . filter ( result , apps . hasAccessTo . bind ( null , req . app ) , function ( error , allowedUsers ) {
2017-03-13 11:09:12 +01:00
if ( error ) return callback ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2017-03-13 11:01:11 +01:00
2018-09-03 15:38:50 +02:00
callback ( null , allowedUsers ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
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 ) {
var min = 0 ;
var max = results . length ;
var cookie = null ;
var 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 ;
var i ;
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 ( ) ;
}
2017-03-13 11:09:12 +01:00
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 ) ;
getUsersWithAccessToApp ( req , function ( error , result ) {
if ( error ) return next ( error ) ;
2017-10-27 01:25:07 +02:00
var results = [ ] ;
2017-03-13 11:09:12 +01:00
// send user objects
2020-02-21 12:17:06 -08:00
result . forEach ( function ( user ) {
2017-03-13 11:09:12 +01:00
// skip entries with empty username. Some apps like owncloud can't deal with this
2020-02-21 12:17:06 -08:00
if ( ! user . username ) return ;
2017-03-13 11:09:12 +01:00
2020-02-21 12:17:06 -08:00
var dn = ldap . parseDN ( 'cn=' + user . id + ',ou=users,dc=cloudron' ) ;
2017-03-13 11:09:12 +01:00
2020-11-12 23:25:33 -08:00
var 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
2020-02-21 12:17:06 -08:00
var displayName = user . displayName || user . username || '' ; // displayName can be empty and username can be null
2017-03-13 11:09:12 +01:00
var nameParts = displayName . split ( ' ' ) ;
var firstName = nameParts [ 0 ] ;
var lastName = nameParts . length > 1 ? nameParts [ nameParts . length - 1 ] : '' ; // choose last part, if it exists
var obj = {
dn : dn . toString ( ) ,
attributes : {
2020-01-05 15:14:44 -08:00
objectclass : [ 'user' , 'inetorgperson' , 'person' ] ,
2017-03-13 11:09:12 +01:00
objectcategory : 'person' ,
2020-02-21 12:17:06 -08:00
cn : user . id ,
uid : user . id ,
entryuuid : user . id , // to support OpenLDAP clients
mail : user . email ,
mailAlternateAddress : user . fallbackEmail ,
2017-03-13 11:09:12 +01:00
displayname : displayName ,
givenName : firstName ,
2020-02-21 12:17:06 -08:00
username : user . username ,
samaccountname : user . username , // to support ActiveDirectory clients
2020-11-12 23:25:33 -08:00
memberof : memberof
2017-03-13 11:09:12 +01: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 ;
// ensure all filter values are also lowercase
var 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 ) ) {
2017-10-27 01:25:07 +02:00
results . push ( obj ) ;
2017-03-13 11:09:12 +01:00
}
} ) ;
2017-10-27 01:25:07 +02:00
finalSend ( results , req , res , next ) ;
2017-03-13 11:09:12 +01:00
} ) ;
}
2016-05-12 13:36:53 -07:00
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
2017-03-13 11:09:12 +01:00
getUsersWithAccessToApp ( req , function ( error , result ) {
2017-03-13 11:06:27 +01:00
if ( error ) return next ( error ) ;
2016-05-12 13:36:53 -07:00
2017-10-27 01:25:07 +02:00
var results = [ ] ;
2017-03-13 11:09:12 +01:00
var groups = [ {
name : 'users' ,
admin : false
} , {
name : 'admins' ,
admin : true
} ] ;
groups . forEach ( function ( group ) {
var dn = ldap . parseDN ( 'cn=' + group . name + ',ou=groups,dc=cloudron' ) ;
2020-02-21 12:17:06 -08:00
var 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
var obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'group' ] ,
cn : group . name ,
memberuid : members . map ( function ( entry ) { return entry . id ; } )
}
} ;
// ensure all filter values are also lowercase
var 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 ) ) {
2017-10-27 01:25:07 +02:00
results . push ( obj ) ;
2017-03-13 11:09:12 +01:00
}
2017-03-13 11:06:27 +01:00
} ) ;
2017-03-13 11:09:12 +01:00
2017-10-27 01:25:07 +02:00
finalSend ( results , req , res , next ) ;
2015-07-20 00:09:47 -07:00
} ) ;
2016-05-12 13:36:53 -07:00
}
2017-10-24 01:35:35 +02:00
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 ) ;
getUsersWithAccessToApp ( req , function ( error , result ) {
if ( error ) return next ( error ) ;
// we only support memberuid here, if we add new group attributes later add them here
if ( req . attribute === 'memberuid' ) {
var found = result . find ( function ( u ) { return u . id === req . value ; } ) ;
if ( found ) return res . end ( true ) ;
}
res . end ( false ) ;
} ) ;
}
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 ) ;
getUsersWithAccessToApp ( req , function ( error , result ) {
if ( error ) return next ( error ) ;
// we only support memberuid here, if we add new group attributes later add them here
if ( req . attribute === 'memberuid' ) {
2020-02-21 12:17:06 -08:00
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
}
res . end ( false ) ;
} ) ;
}
2016-09-26 10:18:58 -07:00
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 ) ;
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 ) {
var email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
var parts = email . split ( '@' ) ;
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2017-10-27 01:25:07 +02:00
2018-05-03 18:05:32 +02:00
mailboxdb . getMailbox ( parts [ 0 ] , parts [ 1 ] , function ( error , mailbox ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2018-05-03 18:05:32 +02:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2016-09-26 21:03:07 -07:00
2018-05-03 18:05:32 +02:00
var obj = {
dn : req . dn . toString ( ) ,
attributes : {
objectclass : [ 'mailbox' ] ,
objectcategory : 'mailbox' ,
cn : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
uid : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
2019-03-22 15:58:53 -07:00
mail : ` ${ mailbox . name } @ ${ mailbox . domain } `
2018-05-03 18:05:32 +02:00
}
} ;
2016-05-29 18:24:54 -07:00
2018-05-03 18:05:32 +02:00
// ensure all filter values are also lowercase
var 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
2018-05-03 18:05:32 +02:00
if ( lowerCaseFilter . matches ( obj . attributes ) ) {
finalSend ( [ obj ] , req , res , next ) ;
} else {
res . end ( ) ;
}
} ) ;
2020-03-06 13:05:31 -08:00
} else if ( req . dn . rdns [ 0 ] . attrs . domain ) { // legacy ldap mailbox search for old sogo
2020-03-06 11:48:51 -08:00
var domain = req . dn . rdns [ 0 ] . attrs . domain . value . toLowerCase ( ) ;
2021-01-07 21:25:38 -08:00
mailboxdb . listMailboxes ( domain , 1 , 1000 , function ( error , mailboxes ) {
2020-03-06 11:48:51 -08:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
var results = [ ] ;
// send mailbox objects
2021-01-07 21:25:38 -08:00
mailboxes . forEach ( function ( mailbox ) {
2020-03-06 11:48:51 -08:00
var dn = ldap . parseDN ( ` cn= ${ mailbox . name } @ ${ domain } ,domain= ${ domain } ,ou=mailboxes,dc=cloudron ` ) ;
var obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'mailbox' ] ,
objectcategory : 'mailbox' ,
cn : ` ${ mailbox . name } @ ${ domain } ` ,
uid : ` ${ mailbox . name } @ ${ domain } ` ,
mail : ` ${ mailbox . name } @ ${ domain } `
}
} ;
// ensure all filter values are also lowercase
var 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 ) ;
}
} ) ;
finalSend ( results , req , res , next ) ;
} ) ;
2020-03-06 13:05:31 -08:00
} else { // new sogo
2020-03-05 22:40:25 -08:00
mailboxdb . listAllMailboxes ( 1 , 1000 , function ( error , mailboxes ) {
2018-05-03 18:05:32 +02:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
var results = [ ] ;
// send mailbox objects
2020-03-05 22:40:25 -08:00
async . eachSeries ( mailboxes , function ( mailbox , callback ) {
var dn = ldap . parseDN ( ` cn= ${ mailbox . name } @ ${ mailbox . domain } ,ou=mailboxes,dc=cloudron ` ) ;
2020-11-12 23:25:33 -08:00
let getFunc = mailbox . ownerType === mail . OWNERTYPE _USER ? users . get : groups . get ;
getFunc ( mailbox . ownerId , function ( error , ownerObject ) {
2020-03-05 22:40:25 -08:00
if ( error ) return callback ( ) ; // skip mailboxes with unknown owner
var obj = {
dn : dn . toString ( ) ,
attributes : {
objectclass : [ 'mailbox' ] ,
objectcategory : 'mailbox' ,
2020-11-12 23:25:33 -08:00
displayname : mailbox . ownerType === mail . OWNERTYPE _USER ? ownerObject . displayName : ownerObject . name ,
2020-03-05 22:40:25 -08:00
cn : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
uid : ` ${ mailbox . name } @ ${ mailbox . domain } ` ,
mail : ` ${ mailbox . name } @ ${ mailbox . domain } `
}
} ;
2021-01-07 21:25:38 -08:00
mailbox . aliases . forEach ( function ( a , idx ) {
obj . attributes [ 'mail' + idx ] = ` ${ a . name } @ ${ a . domain } ` ;
} ) ;
2020-03-05 22:40:25 -08:00
2021-01-07 21:25:38 -08:00
// ensure all filter values are also lowercase
var lowerCaseFilter = safe ( function ( ) { return ldap . parseFilter ( req . filter . toString ( ) . toLowerCase ( ) ) ; } , null ) ;
if ( ! lowerCaseFilter ) return next ( new ldap . OperationsError ( safe . error . toString ( ) ) ) ;
2020-03-05 22:40:25 -08:00
2021-01-07 21:25:38 -08:00
if ( ( req . dn . equals ( dn ) || req . dn . parentOf ( dn ) ) && lowerCaseFilter . matches ( obj . attributes ) ) {
results . push ( obj ) ;
}
2020-03-05 22:40:25 -08:00
2021-01-07 21:25:38 -08:00
callback ( ) ;
2020-03-05 22:40:25 -08:00
} ) ;
} , function ( error ) {
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
finalSend ( results , req , res , next ) ;
2018-05-03 18:05:32 +02:00
} ) ;
} ) ;
}
2016-09-25 18:59:11 -07:00
}
2016-09-26 14:38:23 -07:00
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
2018-01-18 18:14:31 -08:00
var email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
var parts = email . split ( '@' ) ;
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
mailboxdb . getAlias ( parts [ 0 ] , parts [ 1 ] , function ( error , alias ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2016-09-25 18:59:11 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2016-09-27 11:45:49 -07:00
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
2018-01-19 12:10:24 -08:00
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
2016-09-27 11:45:49 -07:00
var obj = {
2016-09-27 11:58:02 -07:00
dn : req . dn . toString ( ) ,
2016-09-27 11:45:49 -07:00
attributes : {
objectclass : [ 'nisMailAlias' ] ,
objectcategory : 'nisMailAlias' ,
2018-01-19 12:10:24 -08:00
cn : ` ${ alias . name } @ ${ alias . domain } ` ,
2020-04-19 18:44:16 -07:00
rfc822MailMember : ` ${ alias . aliasName } @ ${ alias . aliasDomain } `
2016-09-27 11:45:49 -07:00
}
} ;
2016-09-25 18:59:11 -07:00
2016-09-27 11:45:49 -07:00
// ensure all filter values are also lowercase
var 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
2017-10-27 01:25:07 +02:00
if ( lowerCaseFilter . matches ( obj . attributes ) ) {
finalSend ( [ obj ] , req , res , next ) ;
} else {
res . end ( ) ;
}
2016-09-25 18:59:11 -07:00
} ) ;
}
2016-09-27 12:20:20 -07:00
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 ) ;
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
2020-04-17 16:55:23 -07:00
mail . resolveList ( parts [ 0 ] , parts [ 1 ] , function ( error , resolvedMembers , list ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2016-09-25 18:59:11 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2016-09-27 16:27:22 -07:00
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
2018-01-19 11:35:02 -08:00
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
2016-09-27 12:20:20 -07:00
var obj = {
dn : req . dn . toString ( ) ,
attributes : {
objectclass : [ 'mailGroup' ] ,
objectcategory : 'mailGroup' ,
2019-11-06 16:45:44 -08:00
cn : ` ${ name } @ ${ domain } ` , // fully qualified
mail : ` ${ name } @ ${ domain } ` ,
2020-04-18 02:31:59 -07:00
membersOnly : list . membersOnly , // ldapjs only supports strings and string array. so this is not a bool!
2019-11-06 16:45:44 -08:00
mgrpRFC822MailMember : resolvedMembers // fully qualified
2016-09-27 12:20:20 -07:00
}
} ;
2016-05-29 18:24:54 -07:00
2016-09-27 12:20:20 -07:00
// ensure all filter values are also lowercase
var 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
2017-10-27 01:25:07 +02: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
2016-05-29 17:16:52 -07:00
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
2016-09-26 09:08:04 -07:00
var attributeName = Object . keys ( req . dn . rdns [ 0 ] . attrs ) [ 0 ] ;
var 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 ( ) ) ) ;
var api ;
2016-05-16 12:21:15 -07:00
if ( attributeName === 'mail' ) {
2018-04-29 10:58:45 -07:00
api = users . verifyWithEmail ;
2016-05-16 12:21:15 -07:00
} else if ( commonName . indexOf ( '@' ) !== - 1 ) { // if mail is specified, enforce mail check
2018-04-29 10:58:45 -07:00
api = users . verifyWithEmail ;
2016-05-12 13:36:53 -07:00
} else if ( commonName . indexOf ( 'uid-' ) === 0 ) {
2018-04-29 10:58:45 -07:00
api = users . verify ;
2016-05-12 13:36:53 -07:00
} else {
2018-04-29 10:58:45 -07:00
api = users . verifyWithUsername ;
2016-05-12 13:36:53 -07:00
}
2020-01-31 15:28:42 -08:00
api ( commonName , req . credentials || '' , req . app . id , function ( error , user ) {
2019-10-24 14:40:26 -07:00
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 ( ) ) ) ;
2016-06-17 10:08:41 -05:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2016-05-12 13:36:53 -07:00
2016-05-29 17:16:52 -07:00
req . user = user ;
2016-05-12 13:36:53 -07:00
2016-05-29 17:16:52 -07:00
next ( ) ;
} ) ;
}
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
2020-11-20 17:52:22 -08:00
apps . hasAccessTo ( req . app , req . user , function ( error , hasAccess ) {
2018-09-03 15:38:50 +02:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2016-05-12 13:36:53 -07:00
2018-09-03 15:38:50 +02:00
// we return no such object, to avoid leakage of a users existence
2020-11-20 17:52:22 -08:00
if ( ! hasAccess ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2016-05-12 13:36:53 -07:00
2021-01-06 22:09:37 -08:00
eventlog . upsert ( 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
2018-09-03 15:38:50 +02:00
res . end ( ) ;
2016-05-12 13:36:53 -07:00
} ) ;
}
2020-11-12 23:25:33 -08:00
function verifyMailboxPassword ( mailbox , password , callback ) {
assert . strictEqual ( typeof mailbox , 'object' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
if ( mailbox . ownerType === mail . OWNERTYPE _USER ) return users . verify ( mailbox . ownerId , password , users . AP _MAIL /* identifier */ , callback ) ;
groups . getMembers ( mailbox . ownerId , function ( error , userIds ) {
if ( error ) return callback ( error ) ;
let verifiedUser = null ;
async . someSeries ( userIds , function iterator ( userId , iteratorDone ) {
users . verify ( userId , password , users . AP _MAIL /* identifier */ , function ( error , result ) {
if ( error ) return iteratorDone ( null , false ) ;
verifiedUser = result ;
iteratorDone ( null , true ) ;
} ) ;
} , function ( error , result ) {
if ( ! result ) return callback ( new BoxError ( BoxError . INVALID _CREDENTIALS ) ) ;
callback ( null , verifiedUser ) ;
} ) ;
} ) ;
}
2018-12-16 18:04:30 -08:00
function authenticateUserMailbox ( req , res , next ) {
debug ( 'user mailbox auth: %s (from %s)' , req . dn . toString ( ) , req . connection . ldap . id ) ;
if ( ! req . dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
var email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
var parts = email . split ( '@' ) ;
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
mail . getDomain ( parts [ 1 ] , function ( error , domain ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2018-12-16 18:04:30 -08:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
if ( ! domain . enabled ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
mailboxdb . getMailbox ( parts [ 0 ] , parts [ 1 ] , function ( error , mailbox ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2018-12-16 18:04:30 -08:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2020-11-12 23:25:33 -08:00
verifyMailboxPassword ( mailbox , req . credentials || '' , function ( error , result ) {
2019-10-24 14:40:26 -07:00
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 ( ) ) ) ;
2018-12-16 18:04:30 -08:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2021-01-06 22:09:37 -08:00
eventlog . upsert ( eventlog . ACTION _USER _LOGIN , { authType : 'ldap' , mailboxId : email } , { userId : result . id , user : users . removePrivateFields ( result ) } ) ;
2018-12-16 18:04:30 -08:00
res . end ( ) ;
} ) ;
} ) ;
} ) ;
}
2019-04-04 20:46:01 -07:00
function authenticateSftp ( req , res , next ) {
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 ( ) ) ) ;
var email = req . dn . rdns [ 0 ] . attrs . cn . value . toLowerCase ( ) ;
var parts = email . split ( '@' ) ;
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2020-03-26 21:50:25 -07:00
apps . getByFqdn ( parts [ 1 ] , function ( error , app ) {
2019-03-18 21:15:50 -07:00
if ( error ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
2020-03-26 21:50:25 -07:00
users . verifyWithUsername ( parts [ 0 ] , req . credentials , app . id , function ( error ) {
if ( error ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
2019-03-18 21:15:50 -07:00
2020-03-26 21:50:25 -07:00
debug ( 'sftp auth: success' ) ;
res . end ( ) ;
} ) ;
2019-03-18 21:15:50 -07:00
} ) ;
}
2020-10-21 22:31:59 -07:00
function loadSftpConfig ( req , res , next ) {
2021-01-21 12:53:38 -08:00
services . getServiceConfig ( 'sftp' , function ( error , serviceConfig ) {
2020-10-21 22:31:59 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2021-01-21 12:53:38 -08:00
req . requireAdmin = serviceConfig . requireAdmin ;
2020-10-21 22:31:59 -07:00
next ( ) ;
} ) ;
}
2019-04-04 20:46:01 -07:00
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 ) ;
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 ( ) ) ) ;
var parts = req . filter . value . split ( '@' ) ;
if ( parts . length !== 2 ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
var username = parts [ 0 ] ;
2019-03-19 16:23:03 -07:00
var appFqdn = parts [ 1 ] ;
2019-03-18 21:15:50 -07:00
2019-03-19 16:23:03 -07:00
apps . getByFqdn ( appFqdn , function ( error , app ) {
2019-03-18 21:15:50 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2019-03-19 20:47:57 -07:00
// only allow apps which specify "ftp" support in the localstorage addon
2019-04-04 22:38:40 -07:00
if ( ! safe . query ( app . manifest . addons , 'localstorage.ftp.uid' ) ) return next ( new ldap . UnavailableError ( 'Not supported' ) ) ;
2019-04-05 12:59:00 +02:00
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
2019-04-05 12:59:00 +02:00
const uidNumber = app . manifest . addons . localstorage . ftp . uid ;
2019-03-19 21:17:23 -07:00
2019-03-19 16:23:03 -07:00
users . getByUsername ( username , function ( error , user ) {
2019-03-18 21:15:50 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
2020-10-21 22:31:59 -07:00
if ( req . requireAdmin && users . compareRoles ( user . role , users . ROLE _ADMIN ) < 0 ) return next ( new ldap . InsufficientAccessRightsError ( 'Insufficient previleges' ) ) ;
2019-05-22 14:20:52 -07:00
apps . hasAccessTo ( app , user , function ( error , hasAccess ) {
if ( error ) return next ( new ldap . OperationsError ( error . toString ( ) ) ) ;
if ( ! hasAccess ) return next ( new ldap . InsufficientAccessRightsError ( 'Not authorized' ) ) ;
2019-03-18 21:15:50 -07:00
2019-05-22 14:20:52 -07:00
var obj = {
dn : ldap . parseDN ( ` cn= ${ username } @ ${ appFqdn } ,ou=sftp,dc=cloudron ` ) . toString ( ) ,
attributes : {
2020-08-08 18:16:32 -07:00
homeDirectory : path . join ( '/app/data' , app . id ) ,
2019-05-22 14:20:52 -07:00
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
2019-05-22 14:20:52 -07:00
finalSend ( [ obj ] , req , res , next ) ;
} ) ;
2019-03-18 21:15:50 -07:00
} ) ;
} ) ;
}
2020-12-03 12:14:04 -08:00
function verifyAppMailboxPassword ( addonId , username , password , callback ) {
assert . strictEqual ( typeof addonId , 'string' ) ;
assert . strictEqual ( typeof username , 'string' ) ;
assert . strictEqual ( typeof password , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP' ;
appdb . getAppIdByAddonConfigValue ( addonId , ` % ${ pattern } _PASSWORD ` , password , function ( error , appId ) { // search by password because this is unique for each app
if ( error ) return callback ( error ) ;
appdb . getAddonConfig ( appId , addonId , function ( error , result ) {
if ( error ) return callback ( error ) ;
if ( ! result . some ( r => r . name . endsWith ( ` ${ pattern } _USERNAME ` ) && r . value === username ) ) return callback ( new BoxError ( BoxError . INVALID _CREDENTIALS ) ) ;
callback ( null ) ;
} ) ;
} ) ;
}
2018-12-16 18:04:30 -08:00
function authenticateMailAddon ( req , res , next ) {
debug ( 'mail addon auth: %s (from %s)' , req . dn . toString ( ) , req . connection . ldap . id ) ;
2018-02-08 18:49:27 -08:00
2020-12-03 13:35:50 -08:00
if ( ! req . dn . rdns [ 0 ] . attrs . cn ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
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 ( ) ) ) ;
2016-09-26 11:50:32 -07:00
2018-12-06 21:08:19 -08:00
const addonId = req . dn . rdns [ 1 ] . attrs . ou . value . toLowerCase ( ) ; // 'sendmail' or 'recvmail'
2020-12-03 12:14:04 -08:00
if ( addonId !== 'sendmail' && addonId !== 'recvmail' ) return next ( new ldap . OperationsError ( 'Invalid DN' ) ) ;
2018-12-06 21:08:19 -08:00
mail . getDomain ( parts [ 1 ] , function ( error , domain ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2016-09-26 11:50:32 -07:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2018-12-06 21:08:19 -08:00
if ( addonId === 'recvmail' && ! domain . enabled ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2018-01-22 20:35:08 +01:00
2020-12-03 12:14:04 -08:00
verifyAppMailboxPassword ( addonId , email , req . credentials || '' , function ( error ) {
if ( ! error ) return res . end ( ) ; // validated as app
2018-01-22 20:35:08 +01:00
2020-12-03 12:14:04 -08:00
if ( error && error . reason === BoxError . INVALID _CREDENTIALS ) return next ( new ldap . InvalidCredentialsError ( req . dn . toString ( ) ) ) ;
2019-10-24 11:13:48 -07:00
if ( error && error . reason !== BoxError . NOT _FOUND ) return next ( new ldap . OperationsError ( error . message ) ) ;
2018-01-22 20:35:08 +01:00
2018-12-06 21:08:19 -08:00
mailboxdb . getMailbox ( parts [ 0 ] , parts [ 1 ] , function ( error , mailbox ) {
2019-10-24 13:34:14 -07:00
if ( error && error . reason === BoxError . NOT _FOUND ) return next ( new ldap . NoSuchObjectError ( req . dn . toString ( ) ) ) ;
2018-12-06 21:08:19 -08:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2018-01-29 19:29:04 +01:00
2020-11-12 23:25:33 -08:00
verifyMailboxPassword ( mailbox , req . credentials || '' , function ( error , result ) {
2019-10-24 14:40:26 -07:00
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 ( ) ) ) ;
2018-01-22 20:35:08 +01:00
if ( error ) return next ( new ldap . OperationsError ( error . message ) ) ;
2021-01-06 22:09:37 -08:00
eventlog . upsert ( eventlog . ACTION _USER _LOGIN , { authType : 'ldap' , mailboxId : email } , { userId : result . id , user : users . removePrivateFields ( result ) } ) ;
2018-01-22 20:35:08 +01:00
res . end ( ) ;
} ) ;
2018-12-06 21:08:19 -08:00
} ) ;
2018-01-22 20:35:08 +01:00
} ) ;
2016-09-26 11:50:32 -07:00
} ) ;
2016-05-29 17:25:23 -07:00
}
2016-05-12 13:36:53 -07:00
function start ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2016-09-25 16:11:54 -07:00
var logger = {
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)
2018-12-16 18:04:30 -08:00
gServer . bind ( 'ou=mailboxes,dc=cloudron' , authenticateUserMailbox ) ; // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer . search ( 'ou=mailaliases,dc=cloudron' , mailAliasSearch ) ; // haraka
gServer . search ( 'ou=mailinglists,dc=cloudron' , mailingListSearch ) ; // haraka
2016-09-25 18:59:11 -07:00
2020-11-12 22:13:24 -08:00
gServer . bind ( 'ou=recvmail,dc=cloudron' , authenticateMailAddon ) ; // dovecot (IMAP auth)
gServer . bind ( 'ou=sendmail,dc=cloudron' , authenticateMailAddon ) ; // haraka (MSA 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
2020-10-21 22:31:59 -07:00
gServer . search ( 'ou=sftp,dc=cloudron' , loadSftpConfig , 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 ( ) ;
} ) ;
2019-07-25 15:33:34 -07:00
gServer . listen ( constants . LDAP _PORT , '0.0.0.0' , callback ) ;
2015-07-20 00:09:47 -07:00
}
2015-09-14 10:59:05 -07:00
function stop ( callback ) {
2015-09-14 11:09:37 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-09-14 10:59:05 -07:00
2016-09-15 11:53:28 -07:00
if ( gServer ) gServer . close ( ) ;
2015-09-14 10:59:05 -07:00
callback ( ) ;
}