'use strict' const net = require('net') const tap = require('tap') const vasync = require('vasync') const { getSock } = require('./utils') const ldap = require('../lib') const SERVER_PORT = process.env.SERVER_PORT || 1389 const SUFFIX = 'dc=test' tap.beforeEach(function (t) { // We do not need a `.afterEach` to clean up the sock files because that // is done when the server is destroyed. t.context.sock = getSock() }) tap.test('basic create', function (t) { const server = ldap.createServer() t.ok(server) t.end() }) tap.test('connection count', function (t) { const server = ldap.createServer() t.ok(server) server.listen(0, 'localhost', function () { t.ok(true, 'server listening on ' + server.url) server.getConnections(function (err, count) { t.error(err) t.equal(count, 0) const client = ldap.createClient({ url: server.url }) client.on('connect', function () { t.ok(true, 'client connected') server.getConnections(function (err, count) { t.error(err) t.equal(count, 1) client.unbind() server.close(() => t.end()) }) }) }) }) }) tap.test('properties', function (t) { const server = ldap.createServer() t.equal(server.name, 'LDAPServer') // TODO: better test server.maxConnections = 10 t.equal(server.maxConnections, 10) t.equal(server.url, null, 'url empty before bind') // listen on a random port so we have a url server.listen(0, 'localhost', function () { t.ok(server.url) server.close(() => t.end()) }) }) tap.test('listen on unix/named socket', function (t) { const server = ldap.createServer() server.listen(t.context.sock, function () { t.ok(server.url) t.equal(server.url.split(':')[0], 'ldapi') server.close(() => t.end()) }) }) tap.test('listen on static port', function (t) { const server = ldap.createServer() server.listen(SERVER_PORT, '127.0.0.1', function () { const addr = server.address() t.equal(addr.port, parseInt(SERVER_PORT, 10)) t.equal(server.url, `ldap://127.0.0.1:${SERVER_PORT}`) server.close(() => t.end()) }) }) tap.test('listen on ephemeral port', function (t) { const server = ldap.createServer() server.listen(0, 'localhost', function () { const addr = server.address() t.ok(addr.port > 0) t.ok(addr.port < 65535) server.close(() => t.end()) }) }) tap.test('route order', function (t) { function generateHandler (response) { const func = function handler (req, res, next) { res.send({ dn: response, attributes: { } }) res.end() return next() } return func } const server = ldap.createServer() const sock = t.context.sock const dnShort = SUFFIX const dnMed = 'dc=sub,' + SUFFIX const dnLong = 'dc=long,dc=sub,' + SUFFIX // Mount routes out of order server.search(dnMed, generateHandler(dnMed)) server.search(dnShort, generateHandler(dnShort)) server.search(dnLong, generateHandler(dnLong)) server.listen(sock, function () { t.ok(true, 'server listen') const client = ldap.createClient({ socketPath: sock }) client.on('connect', () => { vasync.forEachParallel({ func: runSearch, inputs: [dnShort, dnMed, dnLong] }, function (err) { t.error(err) client.unbind() server.close(() => t.end()) }) }) function runSearch (value, cb) { client.search(value, '(objectclass=*)', function (err, res) { t.error(err) t.ok(res) res.on('searchEntry', function (entry) { t.equal(entry.dn.toString(), value) }) res.on('end', function () { cb() }) }) } }) }) tap.test('route absent', function (t) { const server = ldap.createServer() const DN_ROUTE = 'dc=base' const DN_MISSING = 'dc=absent' server.bind(DN_ROUTE, function (req, res, next) { res.end() return next() }) server.listen(t.context.sock, function () { t.ok(true, 'server startup') vasync.parallel({ funcs: [ function presentBind (cb) { const clt = ldap.createClient({ socketPath: t.context.sock }) clt.bind(DN_ROUTE, '', function (err) { t.notOk(err) clt.unbind() cb() }) }, function absentBind (cb) { const clt = ldap.createClient({ socketPath: t.context.sock }) clt.bind(DN_MISSING, '', function (err) { t.ok(err) t.equal(err.code, ldap.LDAP_NO_SUCH_OBJECT) clt.unbind() cb() }) } ] }, function (err) { t.notOk(err) server.close(() => t.end()) }) }) }) tap.test('route unbind', function (t) { const server = ldap.createServer() server.unbind(function (req, res, next) { t.ok(true, 'server unbind successful') res.end() return next() }) server.listen(t.context.sock, function () { t.ok(true, 'server startup') const client = ldap.createClient({ socketPath: t.context.sock }) client.bind('', '', function (err) { t.error(err, 'client bind error') client.unbind(function (err) { t.error(err, 'client unbind error') server.close(() => t.end()) }) }) }) }) tap.test('bind/unbind identity anonymous', function (t) { const server = ldap.createServer({ connectionRouter: function (c) { server.newConnection(c) server.emit('testconnection', c) } }) server.unbind(function (req, res, next) { t.ok(true, 'server unbind successful') res.end() return next() }) server.bind('', function (req, res, next) { t.ok(true, 'server bind successful') res.end() return next() }) const anonDN = ldap.dn.parse('cn=anonymous') server.listen(t.context.sock, function () { t.ok(true, 'server startup') const client = ldap.createClient({ socketPath: t.context.sock }) server.once('testconnection', (c) => { t.ok(anonDN.equals(c.ldap.bindDN), 'pre bind dn is correct') client.bind('', '', function (err) { t.error(err, 'client anon bind error') t.ok(anonDN.equals(c.ldap.bindDN), 'anon bind dn is correct') client.unbind(function (err) { t.error(err, 'client anon unbind error') t.ok(anonDN.equals(c.ldap.bindDN), 'anon unbind dn is correct') server.close(() => t.end()) }) }) }) }) }) tap.test('bind/unbind identity user', function (t) { const server = ldap.createServer({ connectionRouter: function (c) { server.newConnection(c) server.emit('testconnection', c) } }) server.unbind(function (req, res, next) { t.ok(true, 'server unbind successful') res.end() return next() }) server.bind('', function (req, res, next) { t.ok(true, 'server bind successful') res.end() return next() }) const anonDN = ldap.dn.parse('cn=anonymous') const testDN = ldap.dn.parse('cn=anotheruser') server.listen(t.context.sock, function () { t.ok(true, 'server startup') const client = ldap.createClient({ socketPath: t.context.sock }) server.once('testconnection', (c) => { t.ok(anonDN.equals(c.ldap.bindDN), 'pre bind dn is correct') client.bind(testDN.toString(), 'somesecret', function (err) { t.error(err, 'user bind error') t.ok(testDN.equals(c.ldap.bindDN), 'user bind dn is correct') // check rebinds too client.bind('', '', function (err) { t.error(err, 'client anon bind error') t.ok(anonDN.equals(c.ldap.bindDN), 'anon bind dn is correct') // user rebind client.bind(testDN.toString(), 'somesecret', function (err) { t.error(err, 'user bind error') t.ok(testDN.equals(c.ldap.bindDN), 'user rebind dn is correct') client.unbind(function (err) { t.error(err, 'user unbind error') t.ok(anonDN.equals(c.ldap.bindDN), 'user unbind dn is correct') server.close(() => t.end()) }) }) }) }) }) }) }) tap.test('strict routing', function (t) { const testDN = 'cn=valid' let clt let server const sock = t.context.sock vasync.pipeline({ funcs: [ function setup (_, cb) { server = ldap.createServer({ // strictDN: true - on by default }) // invalid DNs would go to default handler server.search('', function (req, res, next) { t.ok(req.dn) t.equal(typeof (req.dn), 'object') t.equal(req.dn.toString(), testDN) res.end() next() }) server.listen(sock, function () { t.ok(true, 'server startup') clt = ldap.createClient({ socketPath: sock, strictDN: false }) cb() }) }, function testBad (_, cb) { clt.search('not a dn', { scope: 'base' }, function (err, res) { t.error(err) res.once('error', function (err2) { t.ok(err2) t.equal(err2.code, ldap.LDAP_INVALID_DN_SYNTAX) cb() }) res.once('end', function () { t.fail('accepted invalid dn') cb(Error('bogus')) }) }) }, function testGood (_, cb) { clt.search(testDN, { scope: 'base' }, function (err, res) { t.error(err) res.once('error', function (err2) { t.error(err2) cb(err2) }) res.once('end', function (result) { t.ok(result, 'accepted invalid dn') cb() }) }) } ] }, function (err) { t.error(err) if (clt) { clt.destroy() } server.close(() => t.end()) }) }) tap.test('non-strict routing', function (t) { const server = ldap.createServer({ strictDN: false }) const testDN = 'this ain\'t a DN' // invalid DNs go to default handler server.search('', function (req, res, next) { t.ok(req.dn) t.equal(typeof (req.dn), 'string') t.equal(req.dn, testDN) res.end() next() }) server.listen(t.context.sock, function () { t.ok(true, 'server startup') const clt = ldap.createClient({ socketPath: t.context.sock, strictDN: false }) clt.search(testDN, { scope: 'base' }, function (err, res) { t.error(err) res.on('end', function () { clt.destroy() server.close(() => t.end()) }) }) }) }) tap.test('close accept a callback', function (t) { const server = ldap.createServer() // callback is called when the server is closed server.listen(0, function (err) { t.error(err) server.close(function (err) { t.error(err) t.end() }) }) }) tap.test('close without error calls callback', function (t) { const server = ldap.createServer() // when the server is closed without error, the callback parameter is undefined server.listen(1389, '127.0.0.1', function (err) { t.error(err) server.close(function (err) { t.error(err) t.end() }) }) }) tap.test('close passes error to callback', function (t) { const server = ldap.createServer() // when the server is closed with an error, the error is the first parameter of the callback server.close(function (err) { t.ok(err) t.end() }) }) tap.test('multithreading support via external server', function (t) { const serverOptions = { } const server = ldap.createServer(serverOptions) const fauxServer = net.createServer(serverOptions, (connection) => { server.newConnection(connection) }) fauxServer.log = serverOptions.log fauxServer.ldap = { config: serverOptions } t.ok(server) fauxServer.listen(5555, 'localhost', function () { t.ok(true, 'server listening on ' + server.url) t.ok(fauxServer) const client = ldap.createClient({ url: 'ldap://127.0.0.1:5555' }) client.on('connect', function () { t.ok(client) client.unbind() fauxServer.close(() => t.end()) }) }) }) tap.test('multithreading support via hook', function (t) { const serverOptions = { connectionRouter: (connection) => { server.newConnection(connection) } } const server = ldap.createServer(serverOptions) const fauxServer = ldap.createServer(serverOptions) t.ok(server) fauxServer.listen(0, 'localhost', function () { t.ok(true, 'server listening on ' + server.url) t.ok(fauxServer) const client = ldap.createClient({ url: fauxServer.url }) client.on('connect', function () { t.ok(client) client.unbind() fauxServer.close(() => t.end()) }) }) })