// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var net = require('net');
var tls = require('tls');
var util = require('util');

var asn1 = require('asn1');
var VError = require('verror').VError;

var dn = require('./dn');
var dtrace = require('./dtrace');
var errors = require('./errors');
var Protocol = require('./protocol');

var Parser = require('./messages').Parser;
var AbandonResponse = require('./messages/abandon_response');
var AddResponse = require('./messages/add_response');
var BindResponse = require('./messages/bind_response');
var CompareResponse = require('./messages/compare_response');
var DeleteResponse = require('./messages/del_response');
var ExtendedResponse = require('./messages/ext_response');
var LDAPResult = require('./messages/result');
var ModifyResponse = require('./messages/modify_response');
var ModifyDNResponse = require('./messages/moddn_response');
var SearchRequest = require('./messages/search_request');
var SearchResponse = require('./messages/search_response');
var UnbindResponse = require('./messages/unbind_response');

///--- Globals

var Ber = asn1.Ber;
var BerReader = asn1.BerReader;
var DN = dn.DN;

var sprintf = util.format;

///--- Helpers

function mergeFunctionArgs(argv, start, end) {
  assert.ok(argv);

  if (!start)
    start = 0;
  if (!end)
    end = argv.length;

  var handlers = [];

  for (var i = start; i < end; i++) {
    if (argv[i] instanceof Array) {
      var arr = argv[i];
      for (var j = 0; j < arr.length; j++) {
        if (!(arr[j] instanceof Function)) {
          throw new TypeError('Invalid argument type: ' + typeof (arr[j]));
        }
        handlers.push(arr[j]);
      }
    } else if (argv[i] instanceof Function) {
      handlers.push(argv[i]);
    } else {
      throw new TypeError('Invalid argument type: ' + typeof (argv[i]));
    }
  }

  return handlers;
}

function getResponse(req) {
  assert.ok(req);

  var Response;

  switch (req.protocolOp) {
  case Protocol.LDAP_REQ_BIND:
    Response = BindResponse;
    break;
  case Protocol.LDAP_REQ_ABANDON:
    Response = AbandonResponse;
    break;
  case Protocol.LDAP_REQ_ADD:
    Response = AddResponse;
    break;
  case Protocol.LDAP_REQ_COMPARE:
    Response = CompareResponse;
    break;
  case Protocol.LDAP_REQ_DELETE:
    Response = DeleteResponse;
    break;
  case Protocol.LDAP_REQ_EXTENSION:
    Response = ExtendedResponse;
    break;
  case Protocol.LDAP_REQ_MODIFY:
    Response = ModifyResponse;
    break;
  case Protocol.LDAP_REQ_MODRDN:
    Response = ModifyDNResponse;
    break;
  case Protocol.LDAP_REQ_SEARCH:
    Response = SearchResponse;
    break;
  case Protocol.LDAP_REQ_UNBIND:
    Response = UnbindResponse;
    break;
  default:
    return null;
  }
  assert.ok(Response);

  var res = new Response({
    messageID: req.messageID,
    log: req.log,
    attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
  });
  res.connection = req.connection;
  res.logId = req.logId;

  return res;
}

function defaultHandler(req, res, next) {
  assert.ok(req);
  assert.ok(res);
  assert.ok(next);

  res.matchedDN = req.dn.toString();
  res.errorMessage = 'Server method not implemented';
  res.end(errors.LDAP_OTHER);
  return next();
}

function defaultNoOpHandler(req, res, next) {
  assert.ok(req);
  assert.ok(res);
  assert.ok(next);

  res.end();
  return next();
}

function noSuffixHandler(req, res, next) {
  assert.ok(req);
  assert.ok(res);
  assert.ok(next);

  res.errorMessage = 'No tree found for: ' + req.dn.toString();
  res.end(errors.LDAP_NO_SUCH_OBJECT);
  return next();
}

function noExOpHandler(req, res, next) {
  assert.ok(req);
  assert.ok(res);
  assert.ok(next);

  res.errorMessage = req.requestName + ' not supported';
  res.end(errors.LDAP_PROTOCOL_ERROR);
  return next();
}

function fireDTraceProbe(req, res) {
  assert.ok(req);

  req._dtraceId = res._dtraceId = dtrace._nextId();
  var probeArgs = [
    req._dtraceId,
    req.connection.remoteAddress || 'localhost',
    req.connection.ldap.bindDN.toString(),
    req.dn.toString()
  ];

  var op;
  switch (req.protocolOp) {
  case Protocol.LDAP_REQ_ABANDON:
    op = 'abandon';
    break;
  case Protocol.LDAP_REQ_ADD:
    op = 'add';
    probeArgs.push(req.attributes.length);
    break;
  case Protocol.LDAP_REQ_BIND:
    op = 'bind';
    break;
  case Protocol.LDAP_REQ_COMPARE:
    op = 'compare';
    probeArgs.push(req.attribute);
    probeArgs.push(req.value);
    break;
  case Protocol.LDAP_REQ_DELETE:
    op = 'delete';
    break;
  case Protocol.LDAP_REQ_EXTENSION:
    op = 'exop';
    probeArgs.push(req.name);
    probeArgs.push(req.value);
    break;
  case Protocol.LDAP_REQ_MODIFY:
    op = 'modify';
    probeArgs.push(req.changes.length);
    break;
  case Protocol.LDAP_REQ_MODRDN:
    op = 'modifydn';
    probeArgs.push(req.newRdn.toString());
    probeArgs.push((req.newSuperior ? req.newSuperior.toString() : ''));
    break;
  case Protocol.LDAP_REQ_SEARCH:
    op = 'search';
    probeArgs.push(req.scope);
    probeArgs.push(req.filter.toString());
    break;
  case Protocol.LDAP_REQ_UNBIND:
    op = 'unbind';
    break;
  default:
    break;
  }

  res._dtraceOp = op;
  dtrace.fire('server-' + op + '-start', function () {
    return probeArgs;
  });
}

///--- API

/**
 * Constructs a new server that you can call .listen() on, in the various
 * forms node supports.  You need to first assign some handlers to the various
 * LDAP operations however.
 *
 * The options object currently only takes a certificate/private key, and a
 * bunyan logger handle.
 *
 * This object exposes the following events:
 *  - 'error'
 *  - 'close'
 *
 * @param {Object} options (optional) parameterization object.
 * @throws {TypeError} on bad input.
 */
function Server(options) {
  if (options) {
    if (typeof (options) !== 'object')
      throw new TypeError('options (object) required');
    if (typeof (options.log) !== 'object')
      throw new TypeError('options.log must be an object');

    if (options.certificate || options.key) {
      if (!(options.certificate && options.key) ||
          (typeof (options.certificate) !== 'string' &&
          !Buffer.isBuffer(options.certificate)) ||
          (typeof (options.key) !== 'string' &&
          !Buffer.isBuffer(options.key))) {
        throw new TypeError('options.certificate and options.key ' +
                            '(string or buffer) are both required for TLS');
      }
    }
  } else {
    options = {};
  }
  var self = this;

  EventEmitter.call(this, options);

  this._chain = [];
  this.log = options.log;
  this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true;

  var log = this.log;

  function setupConnection(c) {
    assert.ok(c);

    if (c.type === 'unix') {
      c.remoteAddress = self.server.path;
      c.remotePort = c.fd;
    } else if (c.socket) {
      // TLS
      c.remoteAddress = c.socket.remoteAddress;
      c.remotePort = c.socket.remotePort;
    }

    var rdn = new dn.RDN({cn: 'anonymous'});

    c.ldap = {
      id: c.remoteAddress + ':' + c.remotePort,
      config: options,
      _bindDN: new DN([rdn])
    };
    c.addListener('timeout', function () {
      log.trace('%s timed out', c.ldap.id);
      c.destroy();
    });
    c.addListener('end', function () {
      log.trace('%s shutdown', c.ldap.id);
    });
    c.addListener('error', function (err) {
      log.warn('%s unexpected connection error', c.ldap.id, err);
      self.emit('clientError', err);
      c.destroy();
    });
    c.addListener('close', function (had_err) {
      log.trace('%s close; had_err=%j', c.ldap.id, had_err);
      c.end();
    });

    c.ldap.__defineGetter__('bindDN', function () {
      return c.ldap._bindDN;
    });
    c.ldap.__defineSetter__('bindDN', function (val) {
      if (!(val instanceof DN))
        throw new TypeError('DN required');

      c.ldap._bindDN = val;
      return val;
    });
    return c;
  }

  function newConnection(c) {
    setupConnection(c);
    log.trace('new connection from %s', c.ldap.id);

    dtrace.fire('server-connection', function () {
      return [c.remoteAddress];
    });

    c.parser = new Parser({
      log: options.log
    });
    c.parser.on('message', function (req) {
      req.connection = c;
      req.logId = c.ldap.id + '::' + req.messageID;
      req.startTime = new Date().getTime();

      if (log.debug())
        log.debug('%s: message received: req=%j', c.ldap.id, req.json);

      var res = getResponse(req);
      if (!res) {
        log.warn('Unimplemented server method: %s', req.type);
        c.destroy();
        return false;
      }

      // parse string DNs for routing/etc
      try {
        switch (req.protocolOp) {
        case Protocol.LDAP_REQ_BIND:
          req.name = dn.parse(req.name);
          break;
        case Protocol.LDAP_REQ_ADD:
        case Protocol.LDAP_REQ_COMPARE:
        case Protocol.LDAP_REQ_DELETE:
          req.entry = dn.parse(req.entry);
          break;
        case Protocol.LDAP_REQ_MODIFY:
          req.object = dn.parse(req.object);
          break;
        case Protocol.LDAP_REQ_MODRDN:
          req.entry = dn.parse(req.entry);
          // TODO: handle newRdn/Superior
          break;
        case Protocol.LDAP_REQ_SEARCH:
          req.baseObject = dn.parse(req.baseObject);
          break;
        default:
          break;
        }
      } catch (e) {
        if (self.strictDN) {
          return res.end(errors.LDAP_INVALID_DN_SYNTAX);
        }
      }

      res.connection = c;
      res.logId = req.logId;
      res.requestDN = req.dn;

      var chain = self._getHandlerChain(req, res);

      var i = 0;
      return function (err) {
        function sendError(err) {
          res.status = err.code || errors.LDAP_OPERATIONS_ERROR;
          res.matchedDN = req.suffix ? req.suffix.toString() : '';
          res.errorMessage = err.message || '';
          return res.end();
        }

        function after() {
          if (!self._postChain || !self._postChain.length)
            return;

          function next() {} // stub out next for the post chain

          self._postChain.forEach(function (c) {
            c.call(self, req, res, next);
          });
        }

        if (err) {
          log.trace('%s sending error: %s', req.logId, err.stack || err);
          self.emit('clientError', err);
          sendError(err);
          return after();
        }

        try {
          var next = arguments.callee;
          if (chain.handlers[i])
            return chain.handlers[i++].call(chain.backend, req, res, next);

          if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0)
            c.ldap.bindDN = req.dn;

          return after();
        } catch (e) {
          if (!e.stack)
            e.stack = e.toString();
          log.error('%s uncaught exception: %s', req.logId, e.stack);
          return sendError(new errors.OperationsError(e.message));
        }

      }();
    });

    c.parser.on('error', function (err, message) {
      self.emit('error', new VError(err, 'Parser error for %s', c.ldap.id));

      if (!message)
        return c.destroy();

      var res = getResponse(message);
      if (!res)
        return c.destroy();

      res.status = 0x02; // protocol error
      res.errorMessage = err.toString();
      return c.end(res.toBer());
    });

    c.on('data', function (data) {
      if (log.trace())
        log.trace('data on %s: %s', c.ldap.id, util.inspect(data));

      c.parser.write(data);
    });

  } // end newConnection

  this.routes = {};
  if ((options.cert || options.certificate) && options.key) {
    options.cert = options.cert || options.certificate;
    this.server = tls.createServer(options, newConnection);
  } else {
    this.server = net.createServer(newConnection);
  }
  this.server.log = options.log;
  this.server.ldap = {
    config: options
  };
  this.server.on('close', function () {
    self.emit('close');
  });
  this.server.on('error', function (err) {
    self.emit('error', err);
  });
}
util.inherits(Server, EventEmitter);
Object.defineProperties(Server.prototype, {
  maxConnections: {
    get: function getMaxConnections() {
      return this.server.maxConnections;
    },
    set: function setMaxConnections(val) {
      this.server.maxConnections = val;
    },
    configurable: false
  },
  connections: {
    get: function getConnections() {
      return this.server.connections;
    },
    configurable: false
  },
  name: {
    get: function getName() {
      return 'LDAPServer';
    },
    configurable: false
  },
  url: {
    get: function getURL() {
      var str;
      var addr = this.server.address();
      if (!addr) {
        return null;
      }
      if (!addr.family) {
        str = 'ldapi://';
        str += this.host.replace(new RegExp('/', 'g'), '%2f');
        return str;
      }
      if (this.server instanceof tls.Server) {
        str = 'ldaps://';
      } else {
        str = 'ldap://';
      }
      str += this.host + ':' + this.port;
      return str;
    },
    configurable: false
  }
});
module.exports = Server;

/**
 * Adds a handler (chain) for the LDAP add method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.add = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_ADD, name, args);
};

/**
 * Adds a handler (chain) for the LDAP bind method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.bind = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_BIND, name, args);
};

/**
 * Adds a handler (chain) for the LDAP compare method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.compare = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_COMPARE, name, args);
};

/**
 * Adds a handler (chain) for the LDAP delete method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.del = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_DELETE, name, args);
};

/**
 * Adds a handler (chain) for the LDAP exop method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name OID to assign this handler chain to.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input.
 */
Server.prototype.exop = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_EXTENSION, name, args, true);
};

/**
 * Adds a handler (chain) for the LDAP modify method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.modify = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_MODIFY, name, args);
};

/**
 * Adds a handler (chain) for the LDAP modifyDN method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.modifyDN = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_MODRDN, name, args);
};

/**
 * Adds a handler (chain) for the LDAP search method.
 *
 * Note that this is of the form f(name, [function]) where the second...N
 * arguments can all either be functions or arrays of functions.
 *
 * @param {String} name the DN to mount this handler chain at.
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.search = function (name) {
  var args = Array.prototype.slice.call(arguments, 1);
  return this._mount(Protocol.LDAP_REQ_SEARCH, name, args);
};

/**
 * Adds a handler (chain) for the LDAP unbind method.
 *
 * This method is different than the others and takes no mount point, as unbind
 * is a connection-wide operation, not constrianed to part of the DIT.
 *
 * @return {Server} this so you can chain calls.
 * @throws {TypeError} on bad input
 */
Server.prototype.unbind = function () {
  var args = Array.prototype.slice.call(arguments, 0);
  return this._mount(Protocol.LDAP_REQ_UNBIND, 'unbind', args, true);
};

Server.prototype.use = function use() {
  var args = Array.prototype.slice.call(arguments);
  var chain = mergeFunctionArgs(args, 0, args.length);
  var self = this;
  chain.forEach(function (c) {
    self._chain.push(c);
  });
};

Server.prototype.after = function () {
  if (!this._postChain)
    this._postChain = [];

  var self = this;
  mergeFunctionArgs(arguments).forEach(function (h) {
    self._postChain.push(h);
  });
};

// All these just reexpose the requisite net.Server APIs
Server.prototype.listen = function (port, host, callback) {
  if (typeof (port) !== 'number' && typeof (port) !== 'string')
    throw new TypeError('port (number or path) required');

  if (typeof (host) === 'function') {
    callback = host;
    host = '0.0.0.0';
  }
  if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
    // Disambiguate between string ports and file paths
    port = parseInt(port, 10);
  }
  var self = this;

  function cbListen() {
    if (typeof (port) === 'number') {
      self.host = self.address().address;
      self.port = self.address().port;
    } else {
      self.host = port;
      self.port = self.server.fd;
    }

    if (typeof (callback) === 'function')
      callback();
  }

  if (typeof (port) === 'number') {
    return this.server.listen(port, host, cbListen);
  } else {
    return this.server.listen(port, cbListen);
  }
};
Server.prototype.listenFD = function (fd) {
  this.host = 'unix-domain-socket';
  this.port = fd;
  return this.server.listenFD(fd);
};
Server.prototype.close = function () {
  return this.server.close();
};
Server.prototype.address = function () {
  return this.server.address();
};

Server.prototype._getRoute = function (_dn, backend) {
  assert.ok(dn);

  if (!backend)
    backend = this;

  var name;
  if (_dn instanceof dn.DN) {
    name = _dn.toString();
  } else {
    name = _dn;
  }

  if (!this.routes[name]) {
    this.routes[name] = {};
    this.routes[name].backend = backend;
    this.routes[name].dn = _dn;
    // Force regeneration of the route key cache on next request
    this._routeKeyCache = null;
  }

  return this.routes[name];
};

Server.prototype._sortedRouteKeys = function _sortedRouteKeys() {
  // The filtered/sorted route keys are cached to prevent needlessly
  // regenerating the list for every incoming request.
  if (!this._routeKeyCache) {
    var self = this;
    var reversedRDNsToKeys = {};
    // Generate mapping of reversedRDNs(DN) -> routeKey
    Object.keys(this.routes).forEach(function (key) {
      var _dn = self.routes[key].dn;
      // Ignore non-DN routes such as exop or unbind
      if (_dn instanceof dn.DN) {
        var reversed = _dn.clone();
        reversed.rdns.reverse();
        reversedRDNsToKeys[reversed.format()] = key;
      }
    });
    var output = [];
    // Reverse-sort on reversedRDS(DN) in order to output routeKey list.
    // This will place more specific DNs in front of their parents:
    // 1. dc=test, dc=domain, dc=sub
    // 2. dc=test, dc=domain
    // 3. dc=other, dc=foobar
    Object.keys(reversedRDNsToKeys).sort().reverse().forEach(function (_dn) {
      output.push(reversedRDNsToKeys[_dn]);
    });
    this._routeKeyCache = output;
  }
  return this._routeKeyCache;
};

Server.prototype._getHandlerChain = function _getHandlerChain(req, res) {
  assert.ok(req);

  fireDTraceProbe(req, res);

  // check anonymous bind
  if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
      req.dn.toString() === '' &&
      req.credentials === '') {
    return {
      backend: self,
      handlers: [defaultNoOpHandler]
    };
  }

  var op = '0x' + req.protocolOp.toString(16);
  var self = this;
  var routes = this.routes;
  var route;

  // Special cases are exops, unbinds and abandons. Handle those first.
  if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
    route = routes[req.requestName];
    if (route) {
      return {
        backend: route.backend,
        handlers: (route[op] ? route[op] : [noExOpHandler])
      };
    } else {
      return {
        backend: self,
        handlers: [noExOpHandler]
      };
    }
  } else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
    route = routes['unbind'];
    if (route) {
      return {
        backend: route.backend,
        handlers: route[op]
      };
    } else {
      return {
        backend: self,
        handlers: [defaultNoOpHandler]
      };
    }
  } else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) {
    return {
      backend: self,
      handlers: [defaultNoOpHandler]
    };
  }

  // Otherwise, match via DN rules
  assert.ok(req.dn);
  var keys = this._sortedRouteKeys();
  var fallbackHandler = [noSuffixHandler];
  // invalid DNs in non-strict mode are routed to the default handler
  var testDN = (typeof (req.dn) === 'string') ? '' : req.dn;

  for (var i = 0; i < keys.length; i++) {
    var suffix = keys[i];
    route = routes[suffix];
    assert.ok(route.dn);
    // Match a valid route or the route wildcard ('')
    if (route.dn.equals(testDN) || route.dn.parentOf(testDN) || suffix === '') {
      if (route[op]) {
        // We should be good to go.
        req.suffix = route.dn;
        return {
          backend: route.backend,
          handlers: route[op]
        };
      } else {
        if (suffix === '') {
          break;
        } else {
          // We found a valid suffix but not a valid operation.
          // There might be a more generic suffix with a legitimate operation.
          fallbackHandler = [defaultHandler];
        }
      }
    }
  }
  return {
    backend: self,
    handlers: fallbackHandler
  };
};

Server.prototype._mount = function (op, name, argv, notDN) {
  assert.ok(op);
  assert.ok(name !== undefined);
  assert.ok(argv);

  if (typeof (name) !== 'string')
    throw new TypeError('name (string) required');
  if (!argv.length)
    throw new Error('at least one handler required');

  var backend = this;
  var index = 0;

  if (typeof (argv[0]) === 'object' && !Array.isArray(argv[0])) {
    backend = argv[0];
    index = 1;
  }
  var route = this._getRoute(notDN ? name : dn.parse(name), backend);

  var chain = this._chain.slice();
  argv.slice(index).forEach(function (a) {
    chain.push(a);
  });
  route['0x' + op.toString(16)] = mergeFunctionArgs(chain);

  return this;
};


// Copyright 2015 Joyent, Inc.

var assert = require('assert');
var util = require('util');

var isDN = require('./dn').DN.isDN;
var isAttribute = require('./attribute').isAttribute;

///--- Helpers

// Copied from mcavage/node-assert-plus
function _assert(arg, type, name) {
  name = name || type;
  throw new assert.AssertionError({
    message: util.format('%s (%s) required', name, type),
    actual: typeof (arg),
    expected: type,
    operator: '===',
    stackStartFunction: _assert.caller
  });
}

///--- API

function stringDN(input, name) {
  if (isDN(input) || typeof (input) === 'string')
    return;
  _assert(input, 'DN or string', name);
}

function optionalStringDN(input, name) {
  if (input === undefined || isDN(input) || typeof (input) === 'string')
    return;
  _assert(input, 'DN or string', name);
}

function optionalDN(input, name) {
  if (input !== undefined && !isDN(input))
    _assert(input, 'DN', name);
}

function optionalArrayOfAttribute(input, name) {
  if (input === undefined)
    return;
  if (!Array.isArray(input) ||
      input.some(function (v) { return !isAttribute(v); })) {
  _assert(input, 'array of Attribute', name);
  }
}

///--- Exports

module.exports = {
  stringDN: stringDN,
  optionalStringDN: optionalStringDN,
  optionalDN: optionalDN,
  optionalArrayOfAttribute: optionalArrayOfAttribute
};


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPResult = require('./result');
var Protocol = require('../protocol');

///--- API

function AddResponse(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = Protocol.LDAP_REP_ADD;
  LDAPResult.call(this, options);
}
util.inherits(AddResponse, LDAPResult);

///--- Exports

module.exports = AddResponse;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var EventEmitter = require('events').EventEmitter;
var util = require('util');

var assert = require('assert-plus');
var asn1 = require('asn1');
var VError = require('verror').VError;

var AbandonRequest = require('./abandon_request');
var AddRequest = require('./add_request');
var AddResponse = require('./add_response');
var BindRequest = require('./bind_request');
var BindResponse = require('./bind_response');
var CompareRequest = require('./compare_request');
var CompareResponse = require('./compare_response');
var DeleteRequest = require('./del_request');
var DeleteResponse = require('./del_response');
var ExtendedRequest = require('./ext_request');
var ExtendedResponse = require('./ext_response');
var ModifyRequest = require('./modify_request');
var ModifyResponse = require('./modify_response');
var ModifyDNRequest = require('./moddn_request');
var ModifyDNResponse = require('./moddn_response');
var SearchRequest = require('./search_request');
var SearchEntry = require('./search_entry');
var SearchReference = require('./search_reference');
var SearchResponse = require('./search_response');
var UnbindRequest = require('./unbind_request');
var UnbindResponse = require('./unbind_response');

var LDAPResult = require('./result');
var Message = require('./message');

var Protocol = require('../protocol');

///--- Globals

var Ber = asn1.Ber;
var BerReader = asn1.BerReader;

///--- API

function Parser(options) {
  assert.object(options);
  assert.object(options.log);

  EventEmitter.call(this);

  this.buffer = null;
  this.log = options.log;
}
util.inherits(Parser, EventEmitter);

Parser.prototype.write = function (data) {
  if (!data || !Buffer.isBuffer(data))
    throw new TypeError('data (buffer) required');

  var nextMessage = null;
  var self = this;

  function end() {
    if (nextMessage)
      return self.write(nextMessage);

    return true;
  }

  self.buffer = (self.buffer ? Buffer.concat([self.buffer, data]) : data);

  var ber = new BerReader(self.buffer);

  var foundSeq = false;
  try {
    foundSeq = ber.readSequence();
  } catch (e) {
    this.emit('error', e);
  }

  if (!foundSeq || ber.remain < ber.length) {
    // ENOTENOUGH
    return false;
  } else if (ber.remain > ber.length) {
    // ETOOMUCH
    // This is sort of ugly, but allows us to make miminal copies
    nextMessage = self.buffer.slice(ber.offset + ber.length);
    ber._size = ber.offset + ber.length;
    assert.equal(ber.remain, ber.length);
  }

  // If we're here, ber holds the message, and nextMessage is temporarily
  // pointing at the next sequence of data (if it exists)
  self.buffer = null;

  var message;
  try {
    // Bail here if peer isn't speaking protocol at all
    message = this.getMessage(ber);

    if (!message) {
      return end();
    }
    message.parse(ber);
  } catch (e) {
    this.emit('error', e, message);
    return false;
  }

  this.emit('message', message);
  return end();
};

Parser.prototype.getMessage = function (ber) {
  assert.ok(ber);

  var self = this;

  var messageID = ber.readInt();
  var type = ber.readSequence();

  var Message;
  switch (type) {

  case Protocol.LDAP_REQ_ABANDON:
    Message = AbandonRequest;
    break;

  case Protocol.LDAP_REQ_ADD:
    Message = AddRequest;
    break;

  case Protocol.LDAP_REP_ADD:
    Message = AddResponse;
    break;

  case Protocol.LDAP_REQ_BIND:
    Message = BindRequest;
    break;

  case Protocol.LDAP_REP_BIND:
    Message = BindResponse;
    break;

  case Protocol.LDAP_REQ_COMPARE:
    Message = CompareRequest;
    break;

  case Protocol.LDAP_REP_COMPARE:
    Message = CompareResponse;
    break;

  case Protocol.LDAP_REQ_DELETE:
    Message = DeleteRequest;
    break;

  case Protocol.LDAP_REP_DELETE:
    Message = DeleteResponse;
    break;

  case Protocol.LDAP_REQ_EXTENSION:
    Message = ExtendedRequest;
    break;

  case Protocol.LDAP_REP_EXTENSION:
    Message = ExtendedResponse;
    break;

  case Protocol.LDAP_REQ_MODIFY:
    Message = ModifyRequest;
    break;

  case Protocol.LDAP_REP_MODIFY:
    Message = ModifyResponse;
    break;

  case Protocol.LDAP_REQ_MODRDN:
    Message = ModifyDNRequest;
    break;

  case Protocol.LDAP_REP_MODRDN:
    Message = ModifyDNResponse;
    break;

  case Protocol.LDAP_REQ_SEARCH:
    Message = SearchRequest;
    break;

  case Protocol.LDAP_REP_SEARCH_ENTRY:
    Message = SearchEntry;
    break;

  case Protocol.LDAP_REP_SEARCH_REF:
    Message = SearchReference;
    break;

  case Protocol.LDAP_REP_SEARCH:
    Message = SearchResponse;
    break;

  case Protocol.LDAP_REQ_UNBIND:
    Message = UnbindRequest;
    break;

  default:
    this.emit('error',
              new Error('Op 0x' + (type ? type.toString(16) : '??') +
                        ' not supported'),
              new LDAPResult({
                messageID: messageID,
                protocolOp: type || Protocol.LDAP_REP_EXTENSION
              }));

    return false;
  }

  return new Message({
    messageID: messageID,
    log: self.log
  });
};

///--- Exports

module.exports = Parser;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Change = require('../change');
var Protocol = require('../protocol');
var lassert = require('../assert');

///--- API

function ModifyRequest(options) {
  options = options || {};
  assert.object(options);
  lassert.optionalStringDN(options.object);
  lassert.optionalArrayOfAttribute(options.attributes);

  options.protocolOp = Protocol.LDAP_REQ_MODIFY;
  LDAPMessage.call(this, options);

  this.object = options.object || null;
  this.changes = options.changes ? options.changes.slice(0) : [];
}
util.inherits(ModifyRequest, LDAPMessage);
Object.defineProperties(ModifyRequest.prototype, {
  type: {
    get: function getType() { return 'ModifyRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.object; },
    configurable: false
  }
});

ModifyRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.object = ber.readString();

  ber.readSequence();
  var end = ber.offset + ber.length;
  while (ber.offset < end) {
    var c = new Change();
    c.parse(ber);
    c.modification.type = c.modification.type.toLowerCase();
    this.changes.push(c);
  }

  this.changes.sort(Change.compare);
  return true;
};

ModifyRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  ber.writeString(this.object.toString());
  ber.startSequence();
  this.changes.forEach(function (c) {
    c.toBer(ber);
  });
  ber.endSequence();

  return ber;
};

ModifyRequest.prototype._json = function (j) {
  assert.ok(j);

  j.object = this.object;
  j.changes = [];

  this.changes.forEach(function (c) {
    j.changes.push(c.json);
  });

  return j;
};

///--- Exports

module.exports = ModifyRequest;


var assert = require('assert-plus');
var util = require('util');

var asn1 = require('asn1');

var Control = require('./control');

///--- Globals

var BerReader = asn1.BerReader;
var BerWriter = asn1.BerWriter;

///--- API

function ServerSideSortingRequestControl(options) {
  assert.optionalObject(options);
  options = options || {};
  options.type = ServerSideSortingRequestControl.OID;
  if (options.value) {
    if (Buffer.isBuffer(options.value)) {
      this.parse(options.value);
    } else if (Array.isArray(options.value)) {
      assert.arrayOfObject(options.value, 'options.value must be Objects');
      for (var i = 0; i < options.value.length; i++) {
        if (!options.value[i].hasOwnProperty('attributeType')) {
          throw new Error('Missing required key: attributeType');
        }
      }
      this._value = options.value;
    } else if (typeof (options.value) === 'object') {
      if (!options.value.hasOwnProperty('attributeType')) {
        throw new Error('Missing required key: attributeType');
      }
      this._value = [options.value];
    } else {
      throw new TypeError('options.value must be a Buffer, Array or Object');
    }
    options.value = null;
  }
  Control.call(this, options);
}
util.inherits(ServerSideSortingRequestControl, Control);
Object.defineProperties(ServerSideSortingRequestControl.prototype, {
  value: {
    get: function () { return this._value || []; },
    configurable: false
  }
});

ServerSideSortingRequestControl.prototype.parse = function parse(buffer) {
  assert.ok(buffer);

  var ber = new BerReader(buffer);
  var item;
  if (ber.readSequence(0x30)) {
    this._value = [];

    while (ber.readSequence(0x30)) {
      item = {};
      item.attributeType = ber.readString(asn1.Ber.OctetString);
      if (ber.peek() == 0x80) {
        item.orderingRule = ber.readString(0x80);
      }
      if (ber.peek() == 0x81) {
        item.reverseOrder = (ber._readTag(0x81) === 0 ? false : true);
      }
      this._value.push(item);
    }
    return true;
  }
  return false;
};

ServerSideSortingRequestControl.prototype._toBer = function (ber) {
  assert.ok(ber);

  if (!this._value || this.value.length === 0)
    return;

  var writer = new BerWriter();
  writer.startSequence(0x30);
  for (var i = 0; i < this.value.length; i++) {
    var item = this.value[i];
    writer.startSequence(0x30);
    if (item.attributeType) {
      writer.writeString(item.attributeType, asn1.Ber.OctetString);
    }
    if (item.orderingRule) {
      writer.writeString(item.orderingRule, 0x80);
    }
    if (item.reverseOrder) {
      writer.writeBoolean(item.reverseOrder, 0x81);
    }
    writer.endSequence();
  }
  writer.endSequence();
  ber.writeBuffer(writer.buffer, 0x04);
};

ServerSideSortingRequestControl.prototype._json = function (obj) {
  obj.controlValue = this.value;
  return obj;
};

ServerSideSortingRequestControl.OID = '1.2.840.113556.1.4.473';

///---Exports

module.exports = ServerSideSortingRequestControl;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var util = require('util');
var assert = require('assert-plus');

var LDAPResult = require('../messages').LDAPResult;

///--- Globals

var CODES = require('./codes');
var ERRORS = [];

///--- Error Base class

function LDAPError(message, dn, caller) {
  if (Error.captureStackTrace)
    Error.captureStackTrace(this, caller || LDAPError);

  this.lde_message = message;
  this.lde_dn = dn;
}
util.inherits(LDAPError, Error);
Object.defineProperties(LDAPError.prototype, {
  name: {
    get: function getName() { return 'LDAPError'; },
    configurable: false
  },
  code: {
    get: function getCode() { return CODES.LDAP_OTHER; },
    configurable: false
  },
  message: {
    get: function getMessage() {
      return this.lde_message || this.name;
    },
    configurable: false
  },
  dn: {
    get: function getDN() {
      return (this.lde_dn ? this.lde_dn.toString() : '');
    },
    configurable: false
  }
});

///--- Exported API

module.exports = {};
module.exports.LDAPError = LDAPError;

// Some whacky games here to make sure all the codes are exported
Object.keys(CODES).forEach(function (code) {
  module.exports[code] = CODES[code];
  if (code === 'LDAP_SUCCESS')
    return;

  var err = '';
  var msg = '';
  var pieces = code.split('_').slice(1);
  for (var i = 0; i < pieces.length; i++) {
    var lc = pieces[i].toLowerCase();
    var key = lc.charAt(0).toUpperCase() + lc.slice(1);
    err += key;
    msg += key + ((i + 1) < pieces.length ? ' ' : '');
  }

  if (!/\w+Error$/.test(err))
    err += 'Error';

  // At this point LDAP_OPERATIONS_ERROR is now OperationsError in $err
  // and 'Operations Error' in $msg
  module.exports[err] = function (message, dn, caller) {
    LDAPError.call(this, message, dn, caller || module.exports[err]);
  };
  module.exports[err].constructor = module.exports[err];
  util.inherits(module.exports[err], LDAPError);
  Object.defineProperties(module.exports[err].prototype, {
    name: {
      get: function getName() { return err; },
      configurable: false
    },
    code: {
      get: function getCode() { return CODES[code]; },
      configurable: false
    }
  });

  ERRORS[CODES[code]] = {
    err: err,
    message: msg
  };
});

module.exports.getError = function (res) {
  assert.ok(res instanceof LDAPResult, 'res (LDAPResult) required');

  var errObj = ERRORS[res.status];
  var E = module.exports[errObj.err];
  return new E(res.errorMessage || errObj.message,
               res.matchedDN || null,
               module.exports.getError);
};

module.exports.getMessage = function (code) {
  assert.number(code, 'code (number) required');

  var errObj = ERRORS[code];
  return (errObj && errObj.message ? errObj.message : '');
};

///--- Custom application errors

function ConnectionError(message) {
  LDAPError.call(this, message, null, ConnectionError);
}
util.inherits(ConnectionError, LDAPError);
module.exports.ConnectionError = ConnectionError;
Object.defineProperties(ConnectionError.prototype, {
  name: {
    get: function () { return 'ConnectionError'; },
    configurable: false
  }
});

function AbandonedError(message) {
  LDAPError.call(this, message, null, AbandonedError);
}
util.inherits(AbandonedError, LDAPError);
module.exports.AbandonedError = AbandonedError;
Object.defineProperties(AbandonedError.prototype, {
  name: {
    get: function () { return 'AbandonedError'; },
    configurable: false
  }
});

function TimeoutError(message) {
  LDAPError.call(this, message, null, TimeoutError);
}
util.inherits(TimeoutError, LDAPError);
module.exports.TimeoutError = TimeoutError;
Object.defineProperties(TimeoutError.prototype, {
  name: {
    get: function () { return 'TimeoutError'; },
    configurable: false
  }
});


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var dtrace = require('../dtrace');

var LDAPMessage = require('./result');
var Protocol = require('../protocol');

///--- API
// Ok, so there's really no such thing as an unbind 'response', but to make
// the framework not suck, I just made this up, and have it stubbed so it's
// not such a one-off.

function UnbindResponse(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = 0;
  LDAPMessage.call(this, options);
}
util.inherits(UnbindResponse, LDAPMessage);
Object.defineProperties(UnbindResponse.prototype, {
  type: {
    get: function getType() { return 'UnbindResponse'; },
    configurable: false
  }
});

/**
 * Special override that just ends the connection, if present.
 *
 * @param {Number} status completely ignored.
 */
UnbindResponse.prototype.end = function (status) {
  assert.ok(this.connection);

  this.log.trace('%s: unbinding!', this.connection.ldap.id);

  this.connection.end();

  var self = this;
  if (self._dtraceOp && self._dtraceId) {
    dtrace.fire('server-' + self._dtraceOp + '-done', function () {
      var c = self.connection || {ldap: {}};
      return [
        self._dtraceId || 0,
        (c.remoteAddress || ''),
        c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
        (self.requestDN ? self.requestDN.toString() : ''),
        0,
        ''
      ];
    });
  }
};

UnbindResponse.prototype._json = function (j) {
  return j;
};

///--- Exports

module.exports = UnbindResponse;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');

///--- API

function ExtendedRequest(options) {
  options = options || {};
  assert.object(options);
  assert.optionalString(options.requestName);
  if (options.requestValue &&
      !(Buffer.isBuffer(options.requestValue) ||
      typeof (options.requestValue) === 'string')) {
    throw new TypeError('options.requestValue must be a buffer or a string');
  }

  options.protocolOp = Protocol.LDAP_REQ_EXTENSION;
  LDAPMessage.call(this, options);

  this.requestName = options.requestName || '';
  this.requestValue = options.requestValue;
}
util.inherits(ExtendedRequest, LDAPMessage);
Object.defineProperties(ExtendedRequest.prototype, {
  type: {
    get: function getType() { return 'ExtendedRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.requestName; },
    configurable: false
  },
  name: {
    get: function getName() { return this.requestName; },
    set: function setName(val) {
      assert.string(val);
      this.requestName = val;
    },
    configurable: false
  },
  value: {
    get: function getValue() { return this.requestValue; },
    set: function setValue(val) {
      if (!(Buffer.isBuffer(val) || typeof (val) === 'string'))
        throw new TypeError('value must be a buffer or a string');

      this.requestValue = val;
    },
    configurable: false
  }
});

ExtendedRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.requestName = ber.readString(0x80);
  if (ber.peek() === 0x81)
    try {
      this.requestValue = ber.readString(0x81);
    } catch (e) {
      this.requestValue = ber.readBuffer(0x81);
    }

  return true;
};

ExtendedRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  ber.writeString(this.requestName, 0x80);
  if (Buffer.isBuffer(this.requestValue)) {
    ber.writeBuffer(this.requestValue, 0x81);
  } else if (typeof (this.requestValue) === 'string') {
    ber.writeString(this.requestValue, 0x81);
  }

  return ber;
};

ExtendedRequest.prototype._json = function (j) {
  assert.ok(j);

  j.requestName = this.requestName;
  j.requestValue = (Buffer.isBuffer(this.requestValue)) ?
    this.requestValue.toString('hex') : this.requestValue;

  return j;
};

///--- Exports

module.exports = ExtendedRequest;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');

var asn1 = require('asn1');

var parents = require('ldap-filter');

var Protocol = require('../protocol');

var Filter = require('./filter');
var AndFilter = require('./and_filter');
var ApproximateFilter = require('./approx_filter');
var EqualityFilter = require('./equality_filter');
var ExtensibleFilter = require('./ext_filter');
var GreaterThanEqualsFilter = require('./ge_filter');
var LessThanEqualsFilter = require('./le_filter');
var NotFilter = require('./not_filter');
var OrFilter = require('./or_filter');
var PresenceFilter = require('./presence_filter');
var SubstringFilter = require('./substr_filter');

///--- Globals

var BerReader = asn1.BerReader;

///--- Internal Parsers

/*
 * A filter looks like this coming in:
 *      Filter ::= CHOICE {
 *              and             [0]     SET OF Filter,
 *              or              [1]     SET OF Filter,
 *              not             [2]     Filter,
 *              equalityMatch   [3]     AttributeValueAssertion,
 *              substrings      [4]     SubstringFilter,
 *              greaterOrEqual  [5]     AttributeValueAssertion,
 *              lessOrEqual     [6]     AttributeValueAssertion,
 *              present         [7]     AttributeType,
 *              approxMatch     [8]     AttributeValueAssertion,
 *              extensibleMatch [9]     MatchingRuleAssertion --v3 only
 *      }
 *
 *      SubstringFilter ::= SEQUENCE {
 *              type               AttributeType,
 *              SEQUENCE OF CHOICE {
 *                      initial          [0] IA5String,
 *                      any              [1] IA5String,
 *                      final            [2] IA5String
 *              }
 *      }
 *
 * The extensibleMatch was added in LDAPv3:
 *
 *      MatchingRuleAssertion ::= SEQUENCE {
 *              matchingRule    [1] MatchingRuleID OPTIONAL,
 *              type            [2] AttributeDescription OPTIONAL,
 *              matchValue      [3] AssertionValue,
 *              dnAttributes    [4] BOOLEAN DEFAULT FALSE
 *      }
 */
function _parse(ber) {
  assert.ok(ber);

  function parseSet(f) {
    var end = ber.offset + ber.length;
    while (ber.offset < end)
      f.addFilter(_parse(ber));
  }

  var f;

  var type = ber.readSequence();
  switch (type) {

  case Protocol.FILTER_AND:
    f = new AndFilter();
    parseSet(f);
    break;

  case Protocol.FILTER_APPROX:
    f = new ApproximateFilter();
    f.parse(ber);
    break;

  case Protocol.FILTER_EQUALITY:
    f = new EqualityFilter();
    f.parse(ber);
    return f;

  case Protocol.FILTER_EXT:
    f = new ExtensibleFilter();
    f.parse(ber);
    return f;

  case Protocol.FILTER_GE:
    f = new GreaterThanEqualsFilter();
    f.parse(ber);
    return f;

  case Protocol.FILTER_LE:
    f = new LessThanEqualsFilter();
    f.parse(ber);
    return f;

  case Protocol.FILTER_NOT:
    var _f = _parse(ber);
    f = new NotFilter({
      filter: _f
    });
    break;

  case Protocol.FILTER_OR:
    f = new OrFilter();
    parseSet(f);
    break;

  case Protocol.FILTER_PRESENT:
    f = new PresenceFilter();
    f.parse(ber);
    break;

  case Protocol.FILTER_SUBSTRINGS:
    f = new SubstringFilter();
    f.parse(ber);
    break;

  default:
    throw new Error('Invalid search filter type: 0x' + type.toString(16));
  }

  assert.ok(f);
  return f;
}

function cloneFilter(input) {
  var child;
  if (input.type === 'and' || input.type === 'or') {
    child = input.filters.map(cloneFilter);
  } else if (input.type === 'not') {
    child = cloneFilter(input.filter);
  }
  switch (input.type) {
  case 'and':
    return new AndFilter({filters: child});
  case 'or':
    return new OrFilter({filters: child});
  case 'not':
    return new NotFilter({filter: child});
  case 'equal':
    return new EqualityFilter(input);
  case 'substring':
    return new SubstringFilter(input);
  case 'ge':
    return new GreaterThanEqualsFilter(input);
  case 'le':
    return new LessThanEqualsFilter(input);
  case 'present':
    return new PresenceFilter(input);
  case 'approx':
    return new ApproximateFilter(input);
  case 'ext':
    return new ExtensibleFilter(input);
  default:
    throw new Error('invalid filter type:' + input.type);
  }
}

function parseString(str) {
  var generic = parents.parse(str);
  // The filter object(s) return from ldap-filter.parse lack the toBer/parse
  // decoration that native ldapjs filter possess.  cloneFilter adds that back.
  return cloneFilter(generic);
}

///--- API

module.exports = {
  parse: function (ber) {
    if (!ber || !(ber instanceof BerReader))
      throw new TypeError('ber (BerReader) required');

    return _parse(ber);
  },

  parseString: parseString,

  isFilter: Filter.isFilter,

  AndFilter: AndFilter,
  ApproximateFilter: ApproximateFilter,
  EqualityFilter: EqualityFilter,
  ExtensibleFilter: ExtensibleFilter,
  GreaterThanEqualsFilter: GreaterThanEqualsFilter,
  LessThanEqualsFilter: LessThanEqualsFilter,
  NotFilter: NotFilter,
  OrFilter: OrFilter,
  PresenceFilter: PresenceFilter,
  SubstringFilter: SubstringFilter
};


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var asn1 = require('asn1');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');
var dn = require('../dn');
var url = require('../url');

///--- Globals

var BerWriter = asn1.BerWriter;
var parseURL = url.parse;

///--- API

function SearchReference(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = Protocol.LDAP_REP_SEARCH_REF;
  LDAPMessage.call(this, options);

  this.uris = options.uris || [];
}
util.inherits(SearchReference, LDAPMessage);
Object.defineProperties(SearchReference.prototype, {
  type: {
    get: function getType() { return 'SearchReference'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return new dn.DN(''); },
    configurable: false
  },
  object: {
    get: function getObject() {
      return {
        dn: this.dn.toString(),
        uris: this.uris.slice()
      };
    },
    configurable: false
  },
  urls: {
    get: function getUrls() { return this.uris; },
    set: function setUrls(val) {
      assert.ok(val);
      assert.ok(Array.isArray(val));
      this.uris = val.slice();
    },
    configurable: false
  }
});

SearchReference.prototype.toObject = function () {
  return this.object;
};

SearchReference.prototype.fromObject = function (obj) {
  if (typeof (obj) !== 'object')
    throw new TypeError('object required');

  this.uris = obj.uris ? obj.uris.slice() : [];

  return true;
};

SearchReference.prototype._json = function (j) {
  assert.ok(j);
  j.uris = this.uris.slice();
  return j;
};

SearchReference.prototype._parse = function (ber, length) {
  assert.ok(ber);

  while (ber.offset < length) {
    var _url = ber.readString();
    parseURL(_url);
    this.uris.push(_url);
  }

  return true;
};

SearchReference.prototype._toBer = function (ber) {
  assert.ok(ber);

  this.uris.forEach(function (u) {
    ber.writeString(u.href || u);
  });

  return ber;
};

///--- Exports

module.exports = SearchReference;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPResult = require('./result');
var Protocol = require('../protocol');

///--- API

function ExtendedResponse(options) {
  options = options || {};
  assert.object(options);
  assert.optionalString(options.responseName);
  assert.optionalString(options.responsevalue);

  this.responseName = options.responseName || undefined;
  this.responseValue = options.responseValue || undefined;

  options.protocolOp = Protocol.LDAP_REP_EXTENSION;
  LDAPResult.call(this, options);
}
util.inherits(ExtendedResponse, LDAPResult);
Object.defineProperties(ExtendedResponse.prototype, {
  type: {
    get: function getType() { return 'ExtendedResponse'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.responseName; },
    configurable: false
  },
  name: {
    get: function getName() { return this.responseName; },
    set: function setName(val) {
      assert.string(val);
      this.responseName = val;
    },
    configurable: false
  },
  value: {
    get: function getValue() { return this.responseValue; },
    set: function (val) {
      assert.string(val);
      this.responseValue = val;
    },
    configurable: false
  }
});

ExtendedResponse.prototype._parse = function (ber) {
  assert.ok(ber);

  if (!LDAPResult.prototype._parse.call(this, ber))
    return false;

  if (ber.peek() === 0x8a)
    this.responseName = ber.readString(0x8a);
  if (ber.peek() === 0x8b)
    this.responseValue = ber.readString(0x8b);

  return true;
};

ExtendedResponse.prototype._toBer = function (ber) {
  assert.ok(ber);

  if (!LDAPResult.prototype._toBer.call(this, ber))
    return false;

  if (this.responseName)
    ber.writeString(this.responseName, 0x8a);
  if (this.responseValue)
    ber.writeString(this.responseValue, 0x8b);

  return ber;
};

ExtendedResponse.prototype._json = function (j) {
  assert.ok(j);

  j = LDAPResult.prototype._json.call(this, j);

  j.responseName = this.responseName;
  j.responseValue = this.responseValue;

  return j;
};

///--- Exports

module.exports = ExtendedResponse;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPResult = require('./result');
var Protocol = require('../protocol');

///--- API

function CompareResponse(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = Protocol.LDAP_REP_COMPARE;
  LDAPResult.call(this, options);
}
util.inherits(CompareResponse, LDAPResult);

CompareResponse.prototype.end = function (matches) {
  var status = 0x06;
  if (typeof (matches) === 'boolean') {
    if (!matches)
      status = 0x05; // Compare false
  } else {
    status = matches;
  }

  return LDAPResult.prototype.end.call(this, status);
};

///--- Exports

module.exports = CompareResponse;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');
var dn = require('../dn');
var lassert = require('../assert');

///--- API

function ModifyDNRequest(options) {
  options = options || {};
  assert.object(options);
  assert.optionalBool(options.deleteOldRdn);
  lassert.optionalStringDN(options.entry);
  lassert.optionalDN(options.newRdn);
  lassert.optionalDN(options.newSuperior);

  options.protocolOp = Protocol.LDAP_REQ_MODRDN;
  LDAPMessage.call(this, options);

  this.entry = options.entry || null;
  this.newRdn = options.newRdn || null;
  this.deleteOldRdn = options.deleteOldRdn || true;
  this.newSuperior = options.newSuperior || null;
}
util.inherits(ModifyDNRequest, LDAPMessage);
Object.defineProperties(ModifyDNRequest.prototype, {
  type: {
    get: function getType() { return 'ModifyDNRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.entry; },
    configurable: false
  }
});

ModifyDNRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.entry = ber.readString();
  this.newRdn = dn.parse(ber.readString());
  this.deleteOldRdn = ber.readBoolean();
  if (ber.peek() === 0x80)
    this.newSuperior = dn.parse(ber.readString(0x80));

  return true;
};

ModifyDNRequest.prototype._toBer = function (ber) {
  //assert.ok(ber);

  ber.writeString(this.entry.toString());
  ber.writeString(this.newRdn.toString());
  ber.writeBoolean(this.deleteOldRdn);
  if (this.newSuperior) {
    var s = this.newSuperior.toString();
    var len = Buffer.byteLength(s);

    ber.writeByte(0x80); // MODIFY_DN_REQUEST_NEW_SUPERIOR_TAG
    ber.writeByte(len);
    ber._ensure(len);
    ber._buf.write(s, ber._offset);
    ber._offset += len;
  }

  return ber;
};

ModifyDNRequest.prototype._json = function (j) {
  assert.ok(j);

  j.entry = this.entry.toString();
  j.newRdn = this.newRdn.toString();
  j.deleteOldRdn = this.deleteOldRdn;
  j.newSuperior = this.newSuperior ? this.newSuperior.toString() : '';

  return j;
};

///--- Exports

module.exports = ModifyDNRequest;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');

///--- API

function AbandonRequest(options) {
  options = options || {};
  assert.object(options);
  assert.optionalNumber(options.abandonID);

  options.protocolOp = Protocol.LDAP_REQ_ABANDON;
  LDAPMessage.call(this, options);

  this.abandonID = options.abandonID || 0;
}
util.inherits(AbandonRequest, LDAPMessage);
Object.defineProperties(AbandonRequest.prototype, {
  type: {
    get: function getType() { return 'AbandonRequest'; },
    configurable: false
  }
});

AbandonRequest.prototype._parse = function (ber, length) {
  assert.ok(ber);
  assert.ok(length);

  // What a PITA - have to replicate ASN.1 integer logic to work around the
  // way abandon is encoded and the way ldapjs framework handles "normal"
  // messages

  var buf = ber.buffer;
  var offset = 0;
  var value = 0;

  var fb = buf[offset++];
  value = fb & 0x7F;
  for (var i = 1; i < length; i++) {
    value <<= 8;
    value |= (buf[offset++] & 0xff);
  }
  if ((fb & 0x80) == 0x80)
    value = -value;

  ber._offset += length;

  this.abandonID = value;

  return true;
};

AbandonRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  var i = this.abandonID;
  var sz = 4;

  while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000)) &&
         (sz > 1)) {
    sz--;
    i <<= 8;
  }
  assert.ok(sz <= 4);

  while (sz-- > 0) {
    ber.writeByte((i & 0xff000000) >> 24);
    i <<= 8;
  }

  return ber;
};

AbandonRequest.prototype._json = function (j) {
  assert.ok(j);

  j.abandonID = this.abandonID;

  return j;
};

///--- Exports

module.exports = AbandonRequest;


var ldap = require('../lib/index');

///--- Shared handlers

function authorize(req, res, next) {
  /* Any user may search after bind, only cn=root has full power */
  var isSearch = (req instanceof ldap.SearchRequest);
  if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch)
    return next(new ldap.InsufficientAccessRightsError());

  return next();
}

///--- Globals

var SUFFIX = 'o=smartdc';
var db = {};
var server = ldap.createServer();

server.bind('cn=root', function (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, function (req, res, next) {
  var 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, function (req, res, next) {
  var 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, function (req, res, next) {
  var 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));

  var matches = false;
  var vals = db[dn][req.attribute];
  for (var i = 0; i < vals.length; i++) {
    if (vals[i] === req.value) {
      matches = true;
      break;
    }
  }

  res.end(matches);
  return next();
});

server.del(SUFFIX, authorize, function (req, res, next) {
  var dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  delete db[dn];

  res.end();
  return next();
});

server.modify(SUFFIX, authorize, function (req, res, next) {
  var dn = req.dn.toString();
  if (!req.changes.length)
    return next(new ldap.ProtocolError('changes required'));
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  var entry = db[dn];

  for (var i = 0; i < req.changes.length; i++) {
    mod = req.changes[i].modification;
    switch (req.changes[i].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 {
        mod.vals.forEach(function (v) {
          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, function (req, res, next) {
  var dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  var 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 = function (k) {
      if (req.dn.equals(k))
        return true;

      var parent = ldap.parseDN(k).parent();
      return (parent ? parent.equals(req.dn) : false);
    };
    break;

  case 'sub':
    scopeCheck = function (k) {
      return (req.dn.equals(k) || req.dn.parentOf(k));
    };

    break;
  }

  Object.keys(db).forEach(function (key) {
    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, function () {
  console.log('LDAP server up at: %s', server.url);
});


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');

///--- Helpers

function invalidDN(name) {
  var e = new Error();
  e.name = 'InvalidDistinguishedNameError';
  e.message = name;
  return e;
}

function isAlphaNumeric(c) {
  var re = /[A-Za-z0-9]/;
  return re.test(c);
}

function isWhitespace(c) {
  var re = /\s/;
  return re.test(c);
}

function repeatChar(c, n) {
  var out = '';
  var max = n ? n : 0;
  for (var i = 0; i < max; i++)
    out += c;
  return out;
}

///--- API

function RDN(obj) {
  var self = this;
  this.attrs = {};

  if (obj) {
    Object.keys(obj).forEach(function (k) {
      self.set(k, obj[k]);
    });
  }
}

RDN.prototype.set = function rdnSet(name, value, opts) {
  assert.string(name, 'name (string) required');
  assert.string(value, 'value (string) required');

  var self = this;
  var lname = name.toLowerCase();
  this.attrs[lname] = {
    value: value,
    name: name
  };
  if (opts && typeof (opts) === 'object') {
    Object.keys(opts).forEach(function (k) {
      if (k !== 'value')
        self.attrs[lname][k] = opts[k];
    });
  }
};

RDN.prototype.equals = function rdnEquals(rdn) {
  if (typeof (rdn) !== 'object')
    return false;

  var ourKeys = Object.keys(this.attrs);
  var theirKeys = Object.keys(rdn.attrs);
  if (ourKeys.length !== theirKeys.length)
    return false;

  ourKeys.sort();
  theirKeys.sort();

  for (var i = 0; i < ourKeys.length; i++) {
    if (ourKeys[i] !== theirKeys[i])
      return false;
    if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value)
      return false;
  }
  return true;
};

/**
 * Convert RDN to string according to specified formatting options.
 * (see: DN.format for option details)
 */
RDN.prototype.format = function rdnFormat(options) {
  assert.optionalObject(options, 'options must be an object');
  options = options || {};

  var self = this;
  var str = '';

  function escapeValue(val, forceQuote) {
    var out = '';
    var cur = 0;
    var len = val.length;
    var quoted = false;
    /* BEGIN JSSTYLED */
    var escaped = /[\\\"]/;
    var special = /[,=+<>#;]/;
    /* END JSSTYLED */

    if (len > 0) {
      // Wrap strings with trailing or leading spaces in quotes
      quoted = forceQuote || (val[0] == ' ' || val[len-1] == ' ');
    }

    while (cur < len) {
      if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
        out += '\\';
      }
      out += val[cur++];
    }
    if (quoted)
      out = '"' + out + '"';
    return out;
  }
  function sortParsed(a, b) {
    return self.attrs[a].order - self.attrs[b].order;
  }
  function sortStandard(a, b) {
    var nameCompare = a.localeCompare(b);
    if (nameCompare === 0) {
      // TODO: Handle binary values
      return self.attrs[a].value.localeCompare(self.attrs[b].value);
    } else {
      return nameCompare;
    }
  }

  var keys = Object.keys(this.attrs);
  if (options.keepOrder) {
    keys.sort(sortParsed);
  } else {
    keys.sort(sortStandard);
  }

  keys.forEach(function (key) {
    var attr = self.attrs[key];
    if (str.length)
      str += '+';

    if (options.keepCase) {
      str += attr.name;
    } else {
      if (options.upperName)
        str += key.toUpperCase();
      else
        str += key;
    }

    str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted));
  });

  return str;
};

RDN.prototype.toString = function rdnToString() {
  return this.format();
};

// Thank you OpenJDK!
function parse(name) {
  if (typeof (name) !== 'string')
    throw new TypeError('name (string) required');

  var cur = 0;
  var len = name.length;

  function parseRdn() {
    var rdn = new RDN();
    var order = 0;
    rdn.spLead = trim();
    while (cur < len) {
      var opts = {
        order: order
      };
      var attr = parseAttrType();
      trim();
      if (cur >= len || name[cur++] !== '=')
        throw invalidDN(name);

      trim();
      // Parameters about RDN value are set in 'opts' by parseAttrValue
      var value = parseAttrValue(opts);
      rdn.set(attr, value, opts);
      rdn.spTrail = trim();
      if (cur >= len || name[cur] !== '+')
        break;
      ++cur;
      ++order;
    }
    return rdn;
  }

  function trim() {
    var count = 0;
    while ((cur < len) && isWhitespace(name[cur])) {
      ++cur;
      count++;
    }
    return count;
  }

  function parseAttrType() {
    var beg = cur;
    while (cur < len) {
      var c = name[cur];
      if (isAlphaNumeric(c) ||
          c == '.' ||
          c == '-' ||
          c == ' ') {
        ++cur;
      } else {
        break;
      }
    }
    // Back out any trailing spaces.
    while ((cur > beg) && (name[cur - 1] == ' '))
      --cur;

    if (beg == cur)
      throw invalidDN(name);

    return name.slice(beg, cur);
  }

  function parseAttrValue(opts) {
    if (cur < len && name[cur] == '#') {
      opts.binary = true;
      return parseBinaryAttrValue();
    } else if (cur < len && name[cur] == '"') {
      opts.quoted = true;
      return parseQuotedAttrValue();
    } else {
      return parseStringAttrValue();
    }
  }

  function parseBinaryAttrValue() {
    var beg = cur++;
    while (cur < len && isAlphaNumeric(name[cur]))
      ++cur;

    return name.slice(beg, cur);
  }

  function parseQuotedAttrValue() {
    var str = '';
    ++cur; // Consume the first quote

    while ((cur < len) && name[cur] != '"') {
      if (name[cur] === '\\')
        cur++;
      str += name[cur++];
    }
    if (cur++ >= len) // no closing quote
      throw invalidDN(name);

    return str;
  }

  function parseStringAttrValue() {
    var beg = cur;
    var str = '';
    var esc = -1;

    while ((cur < len) && !atTerminator()) {
      if (name[cur] === '\\') {
        // Consume the backslash and mark its place just in case it's escaping
        // whitespace which needs to be preserved.
        esc = cur++;
      }
      if (cur === len) // backslash followed by nothing
        throw invalidDN(name);
      str += name[cur++];
    }

    // Trim off (unescaped) trailing whitespace and rewind cursor to the end of
    // the AttrValue to record whitespace length.
    for (; cur > beg; cur--) {
      if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1)))
        break;
    }
    return str.slice(0, cur - beg);
  }

  function atTerminator() {
    return (cur < len &&
            (name[cur] === ',' ||
             name[cur] === ';' ||
             name[cur] === '+'));
  }

  var rdns = [];

  // Short-circuit for empty DNs
  if (len === 0)
    return new DN(rdns);

  rdns.push(parseRdn());
  while (cur < len) {
    if (name[cur] === ',' || name[cur] === ';') {
      ++cur;
      rdns.push(parseRdn());
    } else {
      throw invalidDN(name);
    }
  }

  return new DN(rdns);
}

function DN(rdns) {
  assert.optionalArrayOfObject(rdns, '[object] required');

  this.rdns = rdns ? rdns.slice() : [];
  this._format = {};
}
Object.defineProperties(DN.prototype, {
  length: {
    get: function getLength() { return this.rdns.length; },
    configurable: false
  }
});

/**
 * Convert DN to string according to specified formatting options.
 *
 * Parameters:
 * - options: formatting parameters (optional, details below)
 *
 * Options are divided into two types:
 * - Preservation options: Using data recorded during parsing, details of the
 *   original DN are preserved when converting back into a string.
 * - Modification options: Alter string formatting defaults.
 *
 * Preservation options _always_ take precedence over modification options.
 *
 * Preservation Options:
 * - keepOrder: Order of multi-value RDNs.
 * - keepQuote: RDN values which were quoted will remain so.
 * - keepSpace: Leading/trailing spaces will be output.
 * - keepCase: Parsed attr name will be output instead of lowercased version.
 *
 * Modification Options:
 * - upperName: RDN names will be uppercased instead of lowercased.
 * - skipSpace: Disable trailing space after RDN separators
 */
DN.prototype.format = function dnFormat(options) {
  assert.optionalObject(options, 'options must be an object');
  options = options || this._format;

  var str = '';
  this.rdns.forEach(function (rdn) {
    var rdnString = rdn.format(options);
    if (str.length !== 0) {
      str += ',';
    }
    if (options.keepSpace) {
      str += (repeatChar(' ', rdn.spLead) +
        rdnString + repeatChar(' ', rdn.spTrail));
    } else if (options.skipSpace === true || str.length === 0) {
      str += rdnString;
    } else {
      str += ' ' + rdnString;
    }
  });
  return str;
};

/**
 * Set default string formatting options.
 */
DN.prototype.setFormat = function setFormat(options) {
  assert.object(options, 'options must be an object');

  this._format = options;
};

DN.prototype.toString = function dnToString() {
  return this.format();
};

DN.prototype.parentOf = function parentOf(dn) {
  if (typeof (dn) !== 'object')
    dn = parse(dn);

  if (this.rdns.length >= dn.rdns.length)
    return false;

  var diff = dn.rdns.length - this.rdns.length;
  for (var i = this.rdns.length - 1; i >= 0; i--) {
    var myRDN = this.rdns[i];
    var theirRDN = dn.rdns[i + diff];

    if (!myRDN.equals(theirRDN))
      return false;
  }

  return true;
};

DN.prototype.childOf = function childOf(dn) {
  if (typeof (dn) !== 'object')
    dn = parse(dn);
  return dn.parentOf(this);
};

DN.prototype.isEmpty = function isEmpty() {
  return (this.rdns.length === 0);
};

DN.prototype.equals = function dnEquals(dn) {
  if (typeof (dn) !== 'object')
    dn = parse(dn);

  if (this.rdns.length !== dn.rdns.length)
    return false;

  for (var i = 0; i < this.rdns.length; i++) {
    if (!this.rdns[i].equals(dn.rdns[i]))
      return false;
  }

  return true;
};

DN.prototype.parent = function dnParent() {
  if (this.rdns.length !== 0) {
    var save = this.rdns.shift();
    var dn = new DN(this.rdns);
    this.rdns.unshift(save);
    return dn;
  }

  return null;
};

DN.prototype.clone = function dnClone() {
  var dn = new DN(this.rdns);
  dn._format = this._format;
  return dn;
};

DN.prototype.reverse = function dnReverse() {
  this.rdns.reverse();
  return this;
};

DN.prototype.pop = function dnPop() {
  return this.rdns.pop();
};

DN.prototype.push = function dnPush(rdn) {
  assert.object(rdn, 'rdn (RDN) required');

  return this.rdns.push(rdn);
};

DN.prototype.shift = function dnShift() {
  return this.rdns.shift();
};

DN.prototype.unshift = function dnUnshift(rdn) {
  assert.object(rdn, 'rdn (RDN) required');

  return this.rdns.unshift(rdn);
};

DN.isDN = function isDN(dn) {
  if (!dn || typeof (dn) !== 'object') {
    return false;
  }
  if (dn instanceof DN) {
    return true;
  }
  if (Array.isArray(dn.rdns)) {
    // Really simple duck-typing for now
    return true;
  }
  return false;
};

///--- Exports

module.exports = {
  parse: parse,
  DN: DN,
  RDN: RDN
};


// Copyright 2014 Joyent, Inc.  All rights reserved.

var EventEmitter = require('events').EventEmitter;
var util = require('util');

var assert = require('assert-plus');

var dn = require('../dn');
var messages = require('../messages/index');
var Protocol = require('../protocol');
var PagedControl = require('../controls/paged_results_control.js');

///--- API

/**
 * Handler object for paged search operations.
 *
 * Provided to consumers in place of the normal search EventEmitter it adds the
 * following new events:
 * 1. page      - Emitted whenever the end of a result page is encountered.
 *                If this is the last page, 'end' will also be emitted.
 *                The event passes two arguments:
 *                1. The result object (similar to 'end')
 *                2. A callback function optionally used to continue the search
 *                   operation if the pagePause option was specified during
 *                   initialization.
 * 2. pageError - Emitted if the server does not support paged search results
 *                If there are no listeners for this event, the 'error' event
 *                will be emitted (and 'end' will not be).  By listening to
 *                'pageError', a successful search that lacks paging will be
 *                able to emit 'end'.
 * 3. search    - Emitted as an internal event to trigger another client search.
 */
function SearchPager(opts) {
  assert.object(opts);
  assert.func(opts.callback);
  assert.number(opts.pageSize);

  EventEmitter.call(this, {});

  this.callback = opts.callback;
  this.controls = opts.controls;
  this.pageSize = opts.pageSize;
  this.pagePause = opts.pagePause;

  this.controls.forEach(function (control) {
    if (control.type === PagedControl.OID) {
      // The point of using SearchPager is not having to do this.
      // Toss an error if the pagedResultsControl is present
      throw new Error('redundant pagedResultControl');
    }
  });

  this.finished = false;
  this.started = false;

  var emitter = new EventEmitter();
  emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'));
  emitter.on('end', this._onEnd.bind(this));
  emitter.on('error',  this._onError.bind(this));
  this.childEmitter = emitter;
}
util.inherits(SearchPager, EventEmitter);
module.exports = SearchPager;

/**
 * Start the paged search.
 */
SearchPager.prototype.begin = function begin() {
  // Starting first page
  this._nextPage(null);
};

SearchPager.prototype._onEnd = function _onEnd(res) {
  var self = this;
  var cookie = null;
  res.controls.forEach(function (control) {
    if (control.type === PagedControl.OID) {
      cookie = control.value.cookie;
    }
  });
  // Pass a noop callback by default for page events
  var nullCb = function () { };

  if (cookie === null) {
    // paged search not supported
    this.finished = true;
    this.emit('page', res, nullCb);
    var err = new Error('missing paged control');
    err.name = 'PagedError';
    if (this.listeners('pageError').length > 0) {
      this.emit('pageError', err);
      // If the consumer as subscribed to pageError, SearchPager is absolved
      // from deliverying the fault via the 'error' event.  Emitting an 'end'
      // event after 'error' breaks the contract that the standard client
      // provides, so it's only a possibility if 'pageError' is used instead.
      this.emit('end', res);
    } else {
      this.emit('error', err);
      // No end event possible per explaination above.
    }
    return;
  }

  if (cookie.length === 0) {
    // end of paged results
    this.finished = true;
    this.emit('page', nullCb);
    this.emit('end', res);
  } else {
    if (this.pagePause) {
      // Wait to fetch next page until callback is invoked
      // Halt page fetching if called with error
      this.emit('page', res, function (err) {
        if (!err) {
          self._nextPage(cookie);
        } else {
          // the paged search has been canceled so emit an end
          self.emit('end', res);
        }
      });
    } else {
      this.emit('page', res, nullCb);
      this._nextPage(cookie);
    }
  }
};

SearchPager.prototype._onError = function _onError(err) {
  this.finished = true;
  this.emit('error', err);
};

/**
 * Initiate a search for the next page using the returned cookie value.
 */
SearchPager.prototype._nextPage = function _nextPage(cookie) {
  var controls = this.controls.slice(0);
  controls.push(new PagedControl({
    value: {
      size: this.pageSize,
      cookie: cookie
    }
  }));

  this.emit('search', controls, this.childEmitter,
      this._sendCallback.bind(this));
};

/**
 * Callback provided to the client API for successful transmission.
 */
SearchPager.prototype._sendCallback = function _sendCallback(err, res) {
  if (err) {
    this.finished = true;
    if (!this.started) {
      // EmitSend error during the first page, bail via callback
      this.callback(err, null);
    } else {
      this.emit('error', err);
    }
  } else {
    // search successfully send
    if (!this.started) {
      this.started = true;
      // send self as emitter as the client would
      this.callback(null, this);
    }
  }
};


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPResult = require('./result');
var Protocol = require('../protocol');

///--- API

function ModifyDNResponse(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = Protocol.LDAP_REP_MODRDN;
  LDAPResult.call(this, options);
}
util.inherits(ModifyDNResponse, LDAPResult);

///--- Exports

module.exports = ModifyDNResponse;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');
var lassert = require('../assert');

///--- API

function DeleteRequest(options) {
  options = options || {};
  assert.object(options);
  lassert.optionalStringDN(options.entry);

  options.protocolOp = Protocol.LDAP_REQ_DELETE;
  LDAPMessage.call(this, options);

  this.entry = options.entry || null;
}
util.inherits(DeleteRequest, LDAPMessage);
Object.defineProperties(DeleteRequest.prototype, {
  type: {
    get: function getType() { return 'DeleteRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.entry; },
    configurable: false
  }
});

DeleteRequest.prototype._parse = function (ber, length) {
  assert.ok(ber);

  this.entry = ber.buffer.slice(0, length).toString('utf8');
  ber._offset += ber.length;

  return true;
};

DeleteRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  var buf = new Buffer(this.entry.toString());
  for (var i = 0; i < buf.length; i++)
    ber.writeByte(buf[i]);

  return ber;
};

DeleteRequest.prototype._json = function (j) {
  assert.ok(j);

  j.entry = this.entry;

  return j;
};

///--- Exports

module.exports = DeleteRequest;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Attribute = require('../attribute');
var Protocol = require('../protocol');
var lassert = require('../assert');

///--- API

function AddRequest(options) {
  options = options || {};
  assert.object(options);
  lassert.optionalStringDN(options.entry);
  lassert.optionalArrayOfAttribute(options.attributes);

  options.protocolOp = Protocol.LDAP_REQ_ADD;
  LDAPMessage.call(this, options);

  this.entry = options.entry || null;
  this.attributes = options.attributes ? options.attributes.slice(0) : [];
}
util.inherits(AddRequest, LDAPMessage);
Object.defineProperties(AddRequest.prototype, {
  type: {
    get: function getType() { return 'AddRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.entry; },
    configurable: false
  }
});

AddRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.entry = ber.readString();

  ber.readSequence();

  var end = ber.offset + ber.length;
  while (ber.offset < end) {
    var a = new Attribute();
    a.parse(ber);
    a.type = a.type.toLowerCase();
    if (a.type === 'objectclass') {
      for (var i = 0; i < a.vals.length; i++)
        a.vals[i] = a.vals[i].toLowerCase();
    }
    this.attributes.push(a);
  }

  this.attributes.sort(Attribute.compare);
  return true;
};

AddRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  ber.writeString(this.entry.toString());
  ber.startSequence();
  this.attributes.forEach(function (a) {
    a.toBer(ber);
  });
  ber.endSequence();

  return ber;
};

AddRequest.prototype._json = function (j) {
  assert.ok(j);

  j.entry = this.entry.toString();
  j.attributes = [];

  this.attributes.forEach(function (a) {
    j.attributes.push(a.json);
  });

  return j;
};

AddRequest.prototype.indexOf = function (attr) {
  if (!attr || typeof (attr) !== 'string')
    throw new TypeError('attr (string) required');

  for (var i = 0; i < this.attributes.length; i++) {
    if (this.attributes[i].type === attr)
      return i;
  }

  return -1;
};

AddRequest.prototype.attributeNames = function () {
  var attrs = [];

  for (var i = 0; i < this.attributes.length; i++)
    attrs.push(this.attributes[i].type.toLowerCase());

  return attrs;
};

AddRequest.prototype.getAttribute = function (name) {
  if (!name || typeof (name) !== 'string')
    throw new TypeError('attribute name (string) required');

  name = name.toLowerCase();

  for (var i = 0; i < this.attributes.length; i++) {
    if (this.attributes[i].type === name)
      return this.attributes[i];
  }

  return null;
};

AddRequest.prototype.addAttribute = function (attr) {
  if (!(attr instanceof Attribute))
    throw new TypeError('attribute (Attribute) required');

  return this.attributes.push(attr);
};

/**
 * Returns a "pure" JS representation of this object.
 *
 * An example object would look like:
 *
 * {
 *   "dn": "cn=unit, dc=test",
 *   "attributes": {
 *     "cn": ["unit", "foo"],
 *     "objectclass": ["top", "person"]
 *   }
 * }
 *
 * @return {Object} that looks like the above.
 */
AddRequest.prototype.toObject = function () {
  var self = this;

  var obj = {
    dn: self.entry ? self.entry.toString() : '',
    attributes: {}
  };

  if (!this.attributes || !this.attributes.length)
    return obj;

  this.attributes.forEach(function (a) {
    if (!obj.attributes[a.type])
      obj.attributes[a.type] = [];

    a.vals.forEach(function (v) {
      if (obj.attributes[a.type].indexOf(v) === -1)
        obj.attributes[a.type].push(v);
    });
  });

  return obj;
};

///--- Exports

module.exports = AddRequest;


// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');

var Logger = require('bunyan');

var Client = require('./client');

///--- Globals

var DEF_LOG = new Logger({
  name: 'ldapjs',
  component: 'client',
  stream: process.stderr,
  serializers: Logger.stdSerializers
});

///--- Functions

function xor() {
  var b = false;
  for (var i = 0; i < arguments.length; i++) {
    if (arguments[i] && !b) {
      b = true;
    } else if (arguments[i] && b) {
      return false;
    }
  }
  return b;
}

///--- Exports

module.exports = {
  Client: Client,
  createClient: function createClient(options) {
    if (typeof (options) !== 'object')
      throw new TypeError('options (object) required');
    if (options.url && typeof (options.url) !== 'string')
      throw new TypeError('options.url (string) required');
    if (options.socketPath && typeof (options.socketPath) !== 'string')
      throw new TypeError('options.socketPath must be a string');
    if (!xor(options.url, options.socketPath))
      throw new TypeError('options.url ^ options.socketPath (String) required');
    if (!options.log)
      options.log = DEF_LOG;
    if (typeof (options.log) !== 'object')
      throw new TypeError('options.log must be an object');

    return new Client(options);
  }
};


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');
var lassert = require('../assert');

///--- API

function CompareRequest(options) {
  options = options || {};
  assert.object(options);
  assert.optionalString(options.attribute);
  assert.optionalString(options.value);
  lassert.optionalStringDN(options.entry);

  options.protocolOp = Protocol.LDAP_REQ_COMPARE;
  LDAPMessage.call(this, options);

  this.entry = options.entry || null;
  this.attribute = options.attribute || '';
  this.value = options.value || '';
}
util.inherits(CompareRequest, LDAPMessage);
Object.defineProperties(CompareRequest.prototype, {
  type: {
    get: function getType() { return 'CompareRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.entry; },
    configurable: false
  }
});

CompareRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.entry = ber.readString();

  ber.readSequence();
  this.attribute = ber.readString().toLowerCase();
  this.value = ber.readString();

  return true;
};

CompareRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  ber.writeString(this.entry.toString());
  ber.startSequence();
  ber.writeString(this.attribute);
  ber.writeString(this.value);
  ber.endSequence();

  return ber;
};

CompareRequest.prototype._json = function (j) {
  assert.ok(j);

  j.entry = this.entry.toString();
  j.attribute = this.attribute;
  j.value = this.value;

  return j;
};

///--- Exports

module.exports = CompareRequest;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var asn1 = require('asn1');

var LDAPMessage = require('./message');
var Protocol = require('../protocol');

///--- Globals

var Ber = asn1.Ber;
var LDAP_BIND_SIMPLE = 'simple';
var LDAP_BIND_SASL = 'sasl';

///--- API

function BindRequest(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = Protocol.LDAP_REQ_BIND;
  LDAPMessage.call(this, options);

  this.version = options.version || 0x03;
  this.name = options.name || null;
  this.authentication = options.authentication || LDAP_BIND_SIMPLE;
  this.credentials = options.credentials || '';
}
util.inherits(BindRequest, LDAPMessage);
Object.defineProperties(BindRequest.prototype, {
  type: {
    get: function getType() { return 'BindRequest'; },
    configurable: false
  },
  _dn: {
    get: function getDN() { return this.name; },
    configurable: false
  }
});

BindRequest.prototype._parse = function (ber) {
  assert.ok(ber);

  this.version = ber.readInt();
  this.name = ber.readString();

  var t = ber.peek();

  // TODO add support for SASL et al
  if (t !== Ber.Context)
    throw new Error('authentication 0x' + t.toString(16) + ' not supported');

  this.authentication = LDAP_BIND_SIMPLE;
  this.credentials = ber.readString(Ber.Context);

  return true;
};

BindRequest.prototype._toBer = function (ber) {
  assert.ok(ber);

  ber.writeInt(this.version);
  ber.writeString((this.name || '').toString());
  // TODO add support for SASL et al
  ber.writeString((this.credentials || ''), Ber.Context);

  return ber;
};

BindRequest.prototype._json = function (j) {
  assert.ok(j);

  j.version = this.version;
  j.name = this.name;
  j.authenticationType = this.authentication;
  j.credentials = this.credentials;

  return j;
};

///--- Exports

module.exports = BindRequest;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var LDAPMessage = require('./result');
var Protocol = require('../protocol');

///--- API

function AbandonResponse(options) {
  options = options || {};
  assert.object(options);

  options.protocolOp = 0;
  LDAPMessage.call(this, options);
}
util.inherits(AbandonResponse, LDAPMessage);
Object.defineProperties(AbandonResponse.prototype, {
  type: {
    get: function getType() { return 'AbandonResponse'; },
    configurable: false
  }
});

AbandonResponse.prototype.end = function (status) {};

AbandonResponse.prototype._json = function (j) {
  return j;
};

///--- Exports

module.exports = AbandonResponse;


var assert = require('assert-plus');
var util = require('util');

var asn1 = require('asn1');

var Control = require('./control');

///--- Globals

var BerReader = asn1.BerReader;
var BerWriter = asn1.BerWriter;

///--- API

function PagedResultsControl(options) {
  assert.optionalObject(options);
  options = options || {};
  options.type = PagedResultsControl.OID;
  if (options.value) {
    if (Buffer.isBuffer(options.value)) {
      this.parse(options.value);
    } else if (typeof (options.value) === 'object') {
      this._value = options.value;
    } else {
      throw new TypeError('options.value must be a Buffer or Object');
    }
    options.value = null;
  }
  Control.call(this, options);
}
util.inherits(PagedResultsControl, Control);
Object.defineProperties(PagedResultsControl.prototype, {
  value: {
    get: function () { return this._value || {}; },
    configurable: false
  }
});

PagedResultsControl.prototype.parse = function parse(buffer) {
  assert.ok(buffer);

  var ber = new BerReader(buffer);
  if (ber.readSequence()) {
    this._value = {};
    this._value.size = ber.readInt();
    this._value.cookie = ber.readString(asn1.Ber.OctetString, true);
     //readString returns '' instead of a zero-length buffer
    if (!this._value.cookie)
      this._value.cookie = new Buffer(0);

    return true;
  }

  return false;
};

PagedResultsControl.prototype._toBer = function (ber) {
  assert.ok(ber);

  if (!this._value)
    return;

  var writer = new BerWriter();
  writer.startSequence();
  writer.writeInt(this.value.size);
  if (this.value.cookie && this.value.cookie.length > 0) {
    writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString);
  } else {
    writer.writeString(''); //writeBuffer rejects zero-length buffers
  }
  writer.endSequence();

  ber.writeBuffer(writer.buffer, 0x04);
};

PagedResultsControl.prototype._json = function (obj) {
  obj.controlValue = this.value;
  return obj;
};

PagedResultsControl.OID = '1.2.840.113556.1.4.319';

///--- Exports
module.exports = PagedResultsControl;


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert');

var asn1 = require('asn1');

var Protocol = require('../protocol');

///--- Globals

var BerWriter = asn1.BerWriter;

var TYPES = {
  'and':       Protocol.FILTER_AND,
  'or':        Protocol.FILTER_OR,
  'not':       Protocol.FILTER_NOT,
  'equal':     Protocol.FILTER_EQUALITY,
  'substring': Protocol.FILTER_SUBSTRINGS,
  'ge':        Protocol.FILTER_GE,
  'le':        Protocol.FILTER_LE,
  'present':   Protocol.FILTER_PRESENT,
  'approx':    Protocol.FILTER_APPROX,
  'ext':       Protocol.FILTER_EXT
};

///--- API

function isFilter(filter) {
  if (!filter || typeof (filter) !== 'object') {
    return false;
  }
  // Do our best to duck-type it
  if (typeof (filter.toBer) === 'function' &&
      typeof (filter.matches) === 'function' &&
      TYPES[filter.type] !== undefined) {
    return true;
  }
  return false;
}

function mixin(target) {
  target.prototype.toBer = function toBer(ber) {
    if (!ber || !(ber instanceof BerWriter))
      throw new TypeError('ber (BerWriter) required');

    ber.startSequence(TYPES[this.type]);
    ber = this._toBer(ber);
    ber.endSequence();
    return ber;
  };
}

module.exports = {
  isFilter: isFilter,
  mixin: mixin
};


// Copyright 2011 Mark Cavage, Inc.  All rights reserved.

var assert = require('assert-plus');
var util = require('util');

var asn1 = require('asn1');

var Control = require('../controls').Control;
var Protocol = require('../protocol');

///--- Globals

var Ber = asn1.Ber;
var BerReader = asn1.BerReader;
var BerWriter = asn1.BerWriter;
var getControl = require('../controls').getControl;

///--- API

/**
 * LDAPMessage structure.
 *
 * @param {Object} options stuff.
 */
function LDAPMessage(options) {
  assert.object(options);

  this.messageID = options.messageID || 0;
  this.protocolOp = options.protocolOp || undefined;
  this.controls = options.controls ? options.controls.slice(0) : [];

  this.log = options.log;
}
Object.defineProperties(LDAPMessage.prototype, {
  id: {
    get: function getId() { return this.messageID; },
    configurable: false
  },
  dn: {
    get: function getDN() { return this._dn || ''; },
    configurable: false
  },
  type: {
    get: function getType() { return 'LDAPMessage'; },
    configurable: false
  },
  json: {
    get: function () {
      var out = this._json({
        messageID: this.messageID,
        protocolOp: this.type
      });
      out.controls = this.controls;
      return out;
    },
    configurable: false
  }
});

LDAPMessage.prototype.toString = function () {
  return JSON.stringify(this.json);
};

LDAPMessage.prototype.parse = function (ber) {
  assert.ok(ber);

  if (this.log.trace())
    this.log.trace('parse: data=%s', util.inspect(ber.buffer));

  // Delegate off to the specific type to parse
  this._parse(ber, ber.length);

  // Look for controls
  if (ber.peek() === 0xa0) {
    ber.readSequence();
    var end = ber.offset + ber.length;
    while (ber.offset < end) {
      var c = getControl(ber);
      if (c)
        this.controls.push(c);
    }
  }

  if (this.log.trace())
    this.log.trace('Parsing done: %j', this.json);
  return true;
};

LDAPMessage.prototype.toBer = function () {
  var writer = new BerWriter();
  writer.startSequence();
  writer.writeInt(this.messageID);

  writer.startSequence(this.protocolOp);
  if (this._toBer)
    writer = this._toBer(writer);
  writer.endSequence();

  if (this.controls && this.controls.length) {
    writer.startSequence(0xa0);
    this.controls.forEach(function (c) {
      c.toBer(writer);
    });
    writer.endSequence();
  }

  writer.endSequence();
  return writer.buffer;
};

///--- Exports

module.exports = LDAPMessage;