ldapjs / node-ldapjs

LDAP Client and Server API for node.js
http://ldapjs.org
MIT License
1.61k stars 440 forks source link

Edited: Ldapjs doesn't work after next.js build #967

Closed carltin0315 closed 7 months ago

carltin0315 commented 7 months ago

UPDATED I found out that if I use next dev to run my app, the ldapjs work. But after I build the app and deploy to prod env, ldapjs can't get any result. Should I report it as a bug?

I'm using ldapjs to perform a ldap authenticationand get all data in the ldap and it work fine on my dev env. But when I deploy to production env , I found that It can pass the authentication which is the bind part, but it doesn't have any return from the searchEntry event.

After the trouble shoot, I figure out that ldapjs library doesn't take the bind as the credential in the search function. It work on my dev env due to my domain login as the credential for the ldap connection, but my production is using local account, thus It can't get any return from ldap.

Is my code have problem or ldapjs do work like that? Can I specific the credential to establish the connection without domain login like a window authentication?

To shorten the code, I only list out the related event, `const ldap = require('ldapjs');

const client = ldap.createClient({ url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389'] });

client.bind('cn=', '', (err) => { const opts={ scope: "sub", attributes:['*'] }

client.search('cn=*', opts, (err,res)=>{ res.on('searchEntry', (entry) => { console.log('entry: ' + JSON.stringify(entry.pojo)); }); }

});`

giovanni-depalma commented 7 months ago

Hello,

I've encountered a similar issue with ldapjs in my project when transitioning from a development environment to a production build. Like you, ldap authentication worked fine in development, but once deployed to production, the search functionality behaved unexpectedly, not returning any results from the searchEntry event.

Through some troubleshooting, I've begun to suspect that the issue may stem from the production build process. It seems possible that class names are being altered during minification, potentially affecting the ldapjs library's ability to correctly perform checks.

In my logs, I noticed the class name change in the end event from SearchResultDone in development to a minified version like s in production, indicating the renaming of classes during the build process.

Specifically, in client.js, the issue seems to involvehow event names are generated and used:

if (msg instanceof SearchEntry || msg instanceof SearchReference) {
  let event = msg.constructor.name
  // Generate the event name for the event emitter, i.e., "searchEntry"
  // and "searchReference".
  event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
  return sendResult(event, msg)
} else {
  tracker.remove(message.messageId)
  // Potentially mark client as idle
  self._updateIdle()
  if (msg instanceof LDAPResult) {
    if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
      return sendResult('error', errors.getError(msg))
    }
    return sendResult('end', msg)
  } else if (msg instanceof Error) {
    return sendResult('error', msg)
  } else {
    return sendResult('error', new errors.ProtocolError(msg.type))
  }
}

This behavior suggests that the process of generating event names based on class names (which are altered in the production build) could lead to events being dispatched with unexpected names

giovanni-depalma commented 7 months ago

As a temporary workaround, I've modified the Client.prototype._sendSocket method directly to ensure that the searchEntry event is emitted without relying on the class name.

const ldap = require("ldapjs");

ldap.Client.prototype._sendSocket = function (
  message,
  expect,
  emitter,
  callback
) {
  const conn = this._socket;
  const tracker = this._tracker;
  const log = this.log;
  const self = this;
  let timer = false;
  let sentEmitter = false;

  function sendResult(event, obj) {
    if (event === "error") {
      self.emit("resultError", obj);
    }
    if (emitter) {
      if (event === "error") {
        // Error will go unhandled if emitter hasn't been sent via callback.
        // Execute callback with the error instead.
        if (!sentEmitter) {
          return callback(obj);
        }
      }
      return emitter.emit(event, obj);
    }

    if (event === "error") {
      return callback(obj);
    }

    return callback(null, obj);
  }

  function messageCallback(msg) {
    if (timer) {
      clearTimeout(timer);
    }

    log.trace({ msg: msg ? msg.pojo : null }, "response received");

    if (expect === "abandon") {
      return sendResult("end", null);
    }

    if (msg instanceof ldap.SearchEntry) {
      return sendResult("searchEntry", msg);
    } else if (msg instanceof ldap.SearchReference) {
      return sendResult("searchReference", msg);
    } else {
      tracker.remove(message.messageId);
      // Potentially mark client as idle
      self._updateIdle();

      if (msg instanceof ldap.LDAPResult) {
        if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
          return sendResult("error", ldap.getError(msg));
        }
        return sendResult("end", msg);
      } else if (msg instanceof Error) {
        return sendResult("error", msg);
      } else {
        return sendResult("error", new ldap.ProtocolError(msg.type));
      }
    }
  }

  function onRequestTimeout() {
    self.emit("timeout", message);
    const { callback: cb } = tracker.fetch(message.messageId);
    if (cb) {
      // FIXME: the timed-out request should be abandoned
      cb(new errors.TimeoutError("request timeout (client interrupt)"));
    }
  }

  function writeCallback() {
    if (expect === "abandon") {
      // Mark the messageId specified as abandoned
      tracker.abandon(message.abandonId);
      // No need to track the abandon request itself
      tracker.remove(message.id);
      return callback(null);
    } else if (expect === "unbind") {
      conn.unbindMessageID = message.id;
      // Mark client as disconnected once unbind clears the socket
      self.connected = false;
      // Some servers will RST the connection after receiving an unbind.
      // Socket errors are blackholed since the connection is being closed.
      conn.removeAllListeners("error");
      conn.on("error", function () {});
      conn.end();
    } else if (emitter) {
      sentEmitter = true;
      callback(null, emitter);
      emitter.emit("searchRequest", message);
      return;
    }
    return false;
  }

  // Start actually doing something...
  tracker.track(message, messageCallback);
  // Mark client as active
  this._updateIdle(true);

  if (self.timeout) {
    log.trace("Setting timeout to %d", self.timeout);
    timer = setTimeout(onRequestTimeout, self.timeout);
  }

  log.trace("sending request %j", message.pojo);

  try {
    const messageBer = message.toBer();
    return conn.write(messageBer.buffer, writeCallback);
  } catch (e) {
    if (timer) {
      clearTimeout(timer);
    }

    log.trace({ err: e }, "Error writing message to socket");
    return callback(e);
  }
};
jsumners commented 7 months ago

We do not support bundlers of any kind.

giovanni-depalma commented 7 months ago

I'm sorry to hear this.

As a suggestion for @carltin0315, you can try to fix the issue by adding this setting into next config (it works for me)

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ["ldapjs"],
  },
};

module.exports = nextConfig;

https://nextjs.org/docs/app/api-reference/next-config-js/serverComponentsExternalPackages

aburd commented 6 months ago

Can confirm the above worked for me.