--- title: Examples | ldapjs --- # ldapjs Examples
This page contains a (hopefully) growing list of sample code to get you started with ldapjs.
# In-memory server ```js const ldap = require('ldapjs'); ///--- Shared handlers function authorize(req, res, next) { /* Any user may search after bind, only cn=root has full power */ const isSearch = (req instanceof ldap.SearchRequest); if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch) return next(new ldap.InsufficientAccessRightsError()); return next(); } ///--- Globals const SUFFIX = 'o=joyent'; const db = {}; const server = ldap.createServer(); server.bind('cn=root', (req, res, next) => { if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') return next(new ldap.InvalidCredentialsError()); res.end(); return next(); }); server.add(SUFFIX, authorize, (req, res, next) => { const dn = req.dn.toString(); if (db[dn]) return next(new ldap.EntryAlreadyExistsError(dn)); db[dn] = req.toObject().attributes; res.end(); return next(); }); server.bind(SUFFIX, (req, res, next) => { const dn = req.dn.toString(); if (!db[dn]) return next(new ldap.NoSuchObjectError(dn)); if (!db[dn].userpassword) return next(new ldap.NoSuchAttributeError('userPassword')); if (db[dn].userpassword.indexOf(req.credentials) === -1) return next(new ldap.InvalidCredentialsError()); res.end(); return next(); }); server.compare(SUFFIX, authorize, (req, res, next) => { const dn = req.dn.toString(); if (!db[dn]) return next(new ldap.NoSuchObjectError(dn)); if (!db[dn][req.attribute]) return next(new ldap.NoSuchAttributeError(req.attribute)); const matches = false; const vals = db[dn][req.attribute]; for (const value of vals) { if (value === req.value) { matches = true; break; } } res.end(matches); return next(); }); server.del(SUFFIX, authorize, (req, res, next) => { const dn = req.dn.toString(); if (!db[dn]) return next(new ldap.NoSuchObjectError(dn)); delete db[dn]; res.end(); return next(); }); server.modify(SUFFIX, authorize, (req, res, next) => { const dn = req.dn.toString(); if (!req.changes.length) return next(new ldap.ProtocolError('changes required')); if (!db[dn]) return next(new ldap.NoSuchObjectError(dn)); const entry = db[dn]; for (const change of req.changes) { mod = change.modification; switch (change.operation) { case 'replace': if (!entry[mod.type]) return next(new ldap.NoSuchAttributeError(mod.type)); if (!mod.vals || !mod.vals.length) { delete entry[mod.type]; } else { entry[mod.type] = mod.vals; } break; case 'add': if (!entry[mod.type]) { entry[mod.type] = mod.vals; } else { for (const v of mod.vals) { if (entry[mod.type].indexOf(v) === -1) entry[mod.type].push(v); } } break; case 'delete': if (!entry[mod.type]) return next(new ldap.NoSuchAttributeError(mod.type)); delete entry[mod.type]; break; } } res.end(); return next(); }); server.search(SUFFIX, authorize, (req, res, next) => { const dn = req.dn.toString(); if (!db[dn]) return next(new ldap.NoSuchObjectError(dn)); let scopeCheck; switch (req.scope) { case 'base': if (req.filter.matches(db[dn])) { res.send({ dn: dn, attributes: db[dn] }); } res.end(); return next(); case 'one': scopeCheck = (k) => { if (req.dn.equals(k)) return true; const parent = ldap.parseDN(k).parent(); return (parent ? parent.equals(req.dn) : false); }; break; case 'sub': scopeCheck = (k) => { return (req.dn.equals(k) || req.dn.parentOf(k)); }; break; } const keys = Object.keys(db); for (const key of keys) { if (!scopeCheck(key)) return; if (req.filter.matches(db[key])) { res.send({ dn: key, attributes: db[key] }); } } res.end(); return next(); }); ///--- Fire it up server.listen(1389, () => { console.log('LDAP server up at: %s', server.url); }); ``` # /etc/passwd server ```js const fs = require('fs'); const ldap = require('ldapjs'); const { spawn } = require('child_process'); ///--- Shared handlers function authorize(req, res, next) { if (!req.connection.ldap.bindDN.equals('cn=root')) return next(new ldap.InsufficientAccessRightsError()); return next(); } function loadPasswdFile(req, res, next) { fs.readFile('/etc/passwd', 'utf8', (err, data) => { if (err) return next(new ldap.OperationsError(err.message)); req.users = {}; const lines = data.split('\n'); for (const line of lines) { if (!line || /^#/.test(line)) continue; const record = line.split(':'); if (!record || !record.length) continue; req.users[record[0]] = { dn: 'cn=' + record[0] + ', ou=users, o=myhost', attributes: { cn: record[0], uid: record[2], gid: record[3], description: record[4], homedirectory: record[5], shell: record[6] || '', objectclass: 'unixUser' } }; } return next(); }); } const pre = [authorize, loadPasswdFile]; ///--- Mainline const server = ldap.createServer(); server.bind('cn=root', (req, res, next) => { if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret') return next(new ldap.InvalidCredentialsError()); res.end(); return next(); }); server.add('ou=users, o=myhost', pre, (req, res, next) => { if (!req.dn.rdns[0].cn) return next(new ldap.ConstraintViolationError('cn required')); if (req.users[req.dn.rdns[0].cn]) return next(new ldap.EntryAlreadyExistsError(req.dn.toString())); const entry = req.toObject().attributes; if (entry.objectclass.indexOf('unixUser') === -1) return next(new ldap.ConstraintViolationError('entry must be a unixUser')); const opts = ['-m']; if (entry.description) { opts.push('-c'); opts.push(entry.description[0]); } if (entry.homedirectory) { opts.push('-d'); opts.push(entry.homedirectory[0]); } if (entry.gid) { opts.push('-g'); opts.push(entry.gid[0]); } if (entry.shell) { opts.push('-s'); opts.push(entry.shell[0]); } if (entry.uid) { opts.push('-u'); opts.push(entry.uid[0]); } opts.push(entry.cn[0]); const useradd = spawn('useradd', opts); const messages = []; useradd.stdout.on('data', (data) => { messages.push(data.toString()); }); useradd.stderr.on('data', (data) => { messages.push(data.toString()); }); useradd.on('exit', (code) => { if (code !== 0) { let msg = '' + code; if (messages.length) msg += ': ' + messages.join(); return next(new ldap.OperationsError(msg)); } res.end(); return next(); }); }); server.modify('ou=users, o=myhost', pre, (req, res, next) => { if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (!req.changes.length) return next(new ldap.ProtocolError('changes required')); const user = req.users[req.dn.rdns[0].cn].attributes; let mod; for (const change of req.changes) { mod = change.modification; switch (change.operation) { case 'replace': if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length) return next(new ldap.UnwillingToPerformError('only password updates ' + 'allowed')); break; case 'add': case 'delete': return next(new ldap.UnwillingToPerformError('only replace allowed')); } } const passwd = spawn('chpasswd', ['-c', 'MD5']); passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8'); passwd.on('exit', (code) => { if (code !== 0) return next(new ldap.OperationsError('' + code)); res.end(); return next(); }); }); server.del('ou=users, o=myhost', pre, (req, res, next) => { if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn]) return next(new ldap.NoSuchObjectError(req.dn.toString())); const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]); const messages = []; userdel.stdout.on('data', (data) => { messages.push(data.toString()); }); userdel.stderr.on('data', (data) => { messages.push(data.toString()); }); userdel.on('exit', (code) => { if (code !== 0) { let msg = '' + code; if (messages.length) msg += ': ' + messages.join(); return next(new ldap.OperationsError(msg)); } res.end(); return next(); }); }); server.search('o=myhost', pre, (req, res, next) => { const keys = Object.keys(req.users); for (const k of keys) { if (req.filter.matches(req.users[k].attributes)) res.send(req.users[k]); } res.end(); return next(); }); // LDAP "standard" listens on 389, but whatever. server.listen(1389, '127.0.0.1', () => { console.log('/etc/passwd LDAP server up at: %s', server.url); }); ``` # Address Book This example is courtesy of [Diogo Resende](https://github.com/dresende) and illustrates setting up an address book for typical mail clients such as Thunderbird or Evolution over a MySQL database. ```js // MySQL test: (create on database 'abook' with username 'abook' and password 'abook') // // CREATE TABLE IF NOT EXISTS `users` ( // `id` int(5) unsigned NOT NULL AUTO_INCREMENT, // `username` varchar(50) NOT NULL, // `password` varchar(50) NOT NULL, // PRIMARY KEY (`id`), // KEY `username` (`username`) // ) ENGINE=InnoDB DEFAULT CHARSET=utf8; // INSERT INTO `users` (`username`, `password`) VALUES // ('demo', 'demo'); // CREATE TABLE IF NOT EXISTS `contacts` ( // `id` int(5) unsigned NOT NULL AUTO_INCREMENT, // `user_id` int(5) unsigned NOT NULL, // `name` varchar(100) NOT NULL, // `email` varchar(255) NOT NULL, // PRIMARY KEY (`id`), // KEY `user_id` (`user_id`) // ) ENGINE=InnoDB DEFAULT CHARSET=utf8; // INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES // (1, 'John Doe', 'john.doe@example.com'), // (1, 'Jane Doe', 'jane.doe@example.com'); // const ldap = require('ldapjs'); const mysql = require("mysql"); const server = ldap.createServer(); const addrbooks = {}; const userinfo = {}; const ldap_port = 389; const basedn = "dc=example, dc=com"; const company = "Example"; const db = mysql.createClient({ user: "abook", password: "abook", database: "abook" }); db.query("SELECT c.*,u.username,u.password " + "FROM contacts c JOIN users u ON c.user_id=u.id", (err, contacts) => { if (err) { console.log("Error fetching contacts", err); process.exit(1); } for (const contact of contacts) { if (!addrbooks.hasOwnProperty(contact.username)) { addrbooks[contact.username] = []; userinfo["cn=" + contact.username + ", " + basedn] = { abook: addrbooks[contact.username], pwd: contact.password }; } const p = contact.name.indexOf(" "); if (p != -1) contact.firstname = contact.name.substr(0, p); p = contact.name.lastIndexOf(" "); if (p != -1) contact.surname = contact.name.substr(p + 1); addrbooks[contact.username].push({ dn: "cn=" + contact.name + ", " + basedn, attributes: { objectclass: [ "top" ], cn: contact.name, mail: contact.email, givenname: contact.firstname, sn: contact.surname, ou: company } }); } server.bind(basedn, (req, res, next) => { const username = req.dn.toString(); const password = req.credentials; if (!userinfo.hasOwnProperty(username) || userinfo[username].pwd != password) { return next(new ldap.InvalidCredentialsError()); } res.end(); return next(); }); server.search(basedn, (req, res, next) => { const binddn = req.connection.ldap.bindDN.toString(); if (userinfo.hasOwnProperty(binddn)) { for (const abook of userinfo[binddn].abook) { if (req.filter.matches(abook.attributes)) res.send(abook); } } res.end(); }); server.listen(ldap_port, () => { console.log("Addressbook started at %s", server.url); }); }); ``` To test out this example, try: ```shell $ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \ -w demo -b "dc=example,dc=com" objectclass=* ``` # Multi-threaded Server This example demonstrates multi-threading via the `cluster` module utilizing a `net` server for initial socket receipt. An alternate example demonstrating use of the `connectionRouter` `serverOptions` hook is available in the `examples` directory. ```js const cluster = require('cluster'); const ldap = require('ldapjs'); const net = require('net'); const os = require('os'); const threads = []; threads.getNext = function () { return (Math.floor(Math.random() * this.length)); }; const serverOptions = { port: 1389 }; if (cluster.isMaster) { const server = net.createServer(serverOptions, (socket) => { socket.pause(); console.log('ldapjs client requesting connection'); let routeTo = threads.getNext(); threads[routeTo].send({ type: 'connection' }, socket); }); for (let i = 0; i < os.cpus().length; i++) { let thread = cluster.fork({ 'id': i }); thread.id = i; thread.on('message', function (msg) { }); threads.push(thread); } server.listen(serverOptions.port, function () { console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port); }); } else { const server = ldap.createServer(serverOptions); let threadId = process.env.id; process.on('message', (msg, socket) => { switch (msg.type) { case 'connection': server.newConnection(socket); socket.resume(); console.log('ldapjs client connection accepted on ' + threadId.toString()); } }); server.search('dc=example', function (req, res, next) { console.log('ldapjs search initiated on ' + threadId.toString()); var obj = { dn: req.dn.toString(), attributes: { objectclass: ['organization', 'top'], o: 'example' } }; if (req.filter.matches(obj.attributes)) res.send(obj); res.end(); }); } ```