const Unit = require('libflitter/Unit') const LDAP = require('ldapjs') const Validator = require('email-validator') const net = require('net') const fs = require('fs') // TODO support logging ALL ldap requests when in DEBUG, not just routed ones // TODO need to support LDAP server auto-discovery/detection features class LDAPServerUnit extends Unit { static get name() { return 'ldap_server' } static get services() { return [...super.services, 'configs', 'express', 'output'] } /** * Get the standard format for LDAP DNs. Can be passed into * ldapjs/DN.format(). * @returns {object} */ standard_format() { return this.configs.get('ldap:server.format') } /** * Get the LDAP.js DN for the user auth base. * @returns {ldap/DN} */ auth_dn() { return this.build_dn(this.config.schema.authentication_base) } group_dn() { return this.build_dn(this.config.schema.group_base) } machine_dn() { return this.build_dn(this.config.schema.machine_base) } machine_group_dn() { return this.build_dn(this.config.schema.machine_group_base) } sudo_dn() { return this.build_dn(this.config.schema.sudo_base) } /** * Get the anonymous DN. * @returns {ldap/DN} */ anonymous() { return LDAP.parseDN('cn=anonymous') } /** * Returns true if the string is a valid e-mail address. * * @see https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript * @param {string} email * @returns {boolean} */ validate_email(email) { return Validator.validate(email) } /** * Build an LDAP.js DN object from a set of string RDNs. * @param {...string} parts * @returns {ldap/DN} */ build_dn(...parts) { parts = parts.flat() parts.push(this.config.schema.base_dc) return LDAP.parseDN(parts.join(',')) } /** * Starts the LDAP server. * @param {module:libflitter/app/FlitterApp~FlitterApp} app * @returns {Promise} */ async go(app) { this.config = this.configs.get('ldap:server') const server_config = {} // If Flitter is configured to use an SSL certificate, // use it to enable LDAPS in the server. if ( this.config.ssl?.enable ) { this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.') server_config.certificate = fs.readFileSync(this.config.ssl.certificate) server_config.key = fs.readFileSync(this.config.ssl.key) } else if ( this.express.use_ssl() ) { this.output.info('Using configured SSL certificate. The LDAP server will require an ldaps:// connection.') server_config.certificate = await this.express.ssl_certificate() server_config.key = await this.express.ssl_key() } this.server = LDAP.createServer(server_config) if ( this.config.max_connections ) { this.server.maxConnections = this.config.max_connections } this.output.info(`Will listen on ${this.config.interface}:${this.config.port}`) if ( await this.port_free() ) { await new Promise((res, rej) => { this.server.listen(this.config.port, this.config.interface, (err) => { this.output.success(`LDAP server listening on port ${this.config.port}...`) res() }) }) } else { this.output.error(`LDAP server port ${this.config.port} is not available. The LDAP server was not started.`) } } async port_free() { return new Promise((res, rej) => { const server = net.createServer() server.once('error', (e) => { res(false) }) server.once('listening', () => { server.close() res(true) }) server.listen(this.config.port) }) } /** * Stops the LDAP server. * @param {module:libflitter/app/FlitterApp~FlitterApp} app * @returns {Promise} */ async cleanup(app) { this.server.close() } } module.exports = exports = LDAPServerUnit