/* See http://www.python-ldap.org/ for details.
 * $Id: LDAPObject.c,v 1.90 2011/04/11 11:29:59 stroeder Exp $ */

#include "common.h"
#include "patchlevel.h"

#include <math.h>
#include <limits.h>
#include "errors.h"
#include "constants.h"
#include "LDAPObject.h"
#include "ldapcontrol.h"
#include "message.h"
#include "berval.h"
#include "options.h"

#ifdef HAVE_SASL
#include <sasl.h>
#endif

static void free_attrs(char***);

/* constructor */

LDAPObject*
newLDAPObject( LDAP* l ) 
{
    LDAPObject* self = (LDAPObject*) PyObject_NEW(LDAPObject, &LDAP_Type);
    if (self == NULL) 
        return NULL;
    self->ldap = l;
    self->_save = NULL;
    self->valid = 1;
    return self;
}

/* destructor */

static void
dealloc( LDAPObject* self )
{
    if (self->ldap) {
        if (self->valid) {
            LDAP_BEGIN_ALLOW_THREADS( self );
            ldap_unbind_ext( self->ldap, NULL, NULL );
            LDAP_END_ALLOW_THREADS( self );
            self->valid = 0;
        }
        self->ldap = NULL;
    }
    PyObject_DEL(self);
}

/*------------------------------------------------------------
 * utility functions
 */

/* 
 * check to see if the LDAPObject is valid, 
 * ie has been opened, and not closed. An exception is set if not valid.
 */

static int
not_valid( LDAPObject* l ) {
    if (l->valid) {
        return 0;
    } else {
        PyErr_SetString( LDAPexception_class, "LDAP connection invalid" );
        return 1;
    }
}

/* free a LDAPMod (complete or partially) allocated in Tuple_to_LDAPMod() */

static void
LDAPMod_DEL( LDAPMod* lm )
{
    Py_ssize_t i;

    if (lm->mod_type)
        PyMem_DEL(lm->mod_type);
    if (lm->mod_bvalues) {
        for (i = 0; lm->mod_bvalues[i]; i++) {
            PyMem_DEL(lm->mod_bvalues[i]);
        }
        PyMem_DEL(lm->mod_bvalues);
    }
    PyMem_DEL(lm);
}

/* 
 * convert a tuple of the form (int,str,[str,...]) 
 * or (str, [str,...]) if no_op is true, into an LDAPMod structure.
 * See ldap_modify(3) for details.
 *
 * NOTE: the resulting LDAPMod structure has pointers directly into
 *       the Python string storage, so LDAPMod structures MUST have a
 *       shorter lifetime than the tuple passed in.
 */

/* XXX - there is no way to pass complex-structured BER objects in here! */

static LDAPMod*
Tuple_to_LDAPMod( PyObject* tup, int no_op ) 
{
    int op;
    char *type;
    PyObject *list, *item;
    LDAPMod *lm = NULL;
    Py_ssize_t i, len, nstrs;

    if (!PyTuple_Check(tup)) {
        PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO",
           "expected a tuple", tup));
        return NULL;
    }

    if (no_op) {
        if (!PyArg_ParseTuple( tup, "sO", &type, &list ))
                return NULL;
        op = 0;
    } else {
        if (!PyArg_ParseTuple( tup, "isO", &op, &type, &list ))
                return NULL;
    }

    lm = PyMem_NEW(LDAPMod, 1);
    if (lm == NULL)
        goto nomem;

    lm->mod_op = op | LDAP_MOD_BVALUES;
    lm->mod_bvalues = NULL;

    len = strlen(type);
    lm->mod_type = PyMem_NEW(char, len + 1);
    if (lm->mod_type == NULL)
        goto nomem;
    memcpy(lm->mod_type, type, len + 1);

    if (list == Py_None) {
        /* None indicates a NULL mod_bvals */
    } else if (PyString_Check(list)) {
        /* Single string is a singleton list */
        lm->mod_bvalues = PyMem_NEW(struct berval *, 2);
        if (lm->mod_bvalues == NULL)
            goto nomem;
        lm->mod_bvalues[0] = PyMem_NEW(struct berval, 1);
        if (lm->mod_bvalues[0] == NULL)
            goto nomem;
        lm->mod_bvalues[1] = NULL;
        lm->mod_bvalues[0]->bv_len = PyString_Size(list);
        lm->mod_bvalues[0]->bv_val = PyString_AsString(list);
    } else if (PySequence_Check(list)) {
        nstrs = PySequence_Length(list);
        lm->mod_bvalues = PyMem_NEW(struct berval *, nstrs + 1);
        if (lm->mod_bvalues == NULL)
            goto nomem;
        for (i = 0; i < nstrs; i++) {
          lm->mod_bvalues[i] = PyMem_NEW(struct berval, 1);
          if (lm->mod_bvalues[i] == NULL)
              goto nomem;
          lm->mod_bvalues[i+1] = NULL;
          item = PySequence_GetItem(list, i);
          if (item == NULL)
              goto error;
          if (!PyString_Check(item)) {
              PyErr_SetObject( PyExc_TypeError, Py_BuildValue( "sO",
                  "expected a string in the list", item));
              Py_DECREF(item);
              goto error;
          }
          lm->mod_bvalues[i]->bv_len = PyString_Size(item);
          lm->mod_bvalues[i]->bv_val = PyString_AsString(item);
          Py_DECREF(item);
        }
        if (nstrs == 0)
            lm->mod_bvalues[0] = NULL;
    }

    return lm;

nomem:
    PyErr_NoMemory();
error:
    if (lm) 
        LDAPMod_DEL(lm);

    return NULL;
}

/* free the structure allocated in List_to_LDAPMods() */

static void
LDAPMods_DEL( LDAPMod** lms ) {
    LDAPMod** lmp;
    for ( lmp = lms; *lmp; lmp++ )
        LDAPMod_DEL( *lmp );
    PyMem_DEL(lms);
}

/* 
 * convert a list of tuples into a LDAPMod*[] array structure 
 * NOTE: list of tuples must live longer than the LDAPMods
 */

static LDAPMod**
List_to_LDAPMods( PyObject *list, int no_op ) {

    Py_ssize_t i, len;
    LDAPMod** lms;
    PyObject *item;

    if (!PySequence_Check(list)) {
        PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO",
                        "expected list of tuples", list ));
        return NULL;
    }

    len = PySequence_Length(list);

    if (len < 0) {
        PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO",
                         "expected list of tuples", list ));
        return NULL;
    }

    lms = PyMem_NEW(LDAPMod *, len + 1);
    if (lms == NULL) 
        goto nomem;

    for (i = 0; i < len; i++) {
        lms[i] = NULL;
        item = PySequence_GetItem(list, i);
        if (item == NULL) 
            goto error;
        lms[i] = Tuple_to_LDAPMod(item, no_op);
        Py_DECREF(item);
        if (lms[i] == NULL)
            goto error;
    }
    lms[len] = NULL;
    return lms;

nomem:
    PyErr_NoMemory();
error:
    if (lms)
        LDAPMods_DEL(lms);
    return NULL;
}

/*
 * convert a python list of strings into an attr list (char*[]).
 * returns 1 if successful, 0 if not (with exception set)
 * XXX the strings should live longer than the resulting attrs pointer.
 */

int
attrs_from_List( PyObject *attrlist, char***attrsp ) {

    char **attrs = NULL;
    Py_ssize_t i, len;
    PyObject *item;

    if (attrlist == Py_None) {
        /* None means a NULL attrlist */
    } else if (PyString_Check(attrlist)) {
        /* caught by John Benninghoff <johnb@netscape.com> */
        PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO",
                  "expected *list* of strings, not a string", attrlist ));
        goto error;
    } else if (PySequence_Check(attrlist)) {
        len = PySequence_Length(attrlist);
        attrs = PyMem_NEW(char *, len + 1);
        if (attrs == NULL)
            goto nomem;

        for (i = 0; i < len; i++) {
            attrs[i] = NULL;
            item = PySequence_GetItem(attrlist, i);
            if (item == NULL)
                goto error;
            if (!PyString_Check(item)) {
                PyErr_SetObject(PyExc_TypeError, Py_BuildValue("sO",
                                "expected string in list", item));
                Py_DECREF(item);
                goto error;
            }
            attrs[i] = PyString_AsString(item);
            Py_DECREF(item);
        }
        attrs[len] = NULL;
    } else {
        PyErr_SetObject( PyExc_TypeError, Py_BuildValue("sO",
                         "expected list of strings or None", attrlist ));
        goto error;
    }

    *attrsp = attrs;
    return 1;

nomem:
    PyErr_NoMemory();
error:
    free_attrs(&attrs);
    return 0;
}

/* free memory allocated from above routine */

static void
free_attrs( char*** attrsp ) {
    char **attrs = *attrsp;

    if (attrs != NULL) {
        PyMem_DEL(attrs);
        *attrsp = NULL;
    }
}

/*------------------------------------------------------------
 * methods
 */

/* ldap_unbind_ext */

static PyObject*
l_ldap_unbind_ext( LDAPObject* self, PyObject* args )
{
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int ldaperror;

    if (!PyArg_ParseTuple( args, "|OO", &serverctrls, &clientctrls)) return NULL;
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_unbind_ext( self->ldap, server_ldcs, client_ldcs );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_unbind_ext" );

    self->valid = 0;
    Py_INCREF(Py_None);
    return Py_None;
}

/* ldap_abandon_ext */

static PyObject*
l_ldap_abandon_ext( LDAPObject* self, PyObject* args )
{
    int msgid;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int ldaperror;

    if (!PyArg_ParseTuple( args, "i|OO", &msgid, &serverctrls, &clientctrls)) return NULL;
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_abandon_ext( self->ldap, msgid, server_ldcs, client_ldcs );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_abandon_ext" );

    Py_INCREF(Py_None);
    return Py_None;
}

/* ldap_add_ext */

static PyObject *
l_ldap_add_ext( LDAPObject* self, PyObject *args )
{
    char *dn;
    PyObject *modlist;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;
    LDAPMod **mods;

    if (!PyArg_ParseTuple( args, "sO|OO", &dn, &modlist, &serverctrls, &clientctrls )) return NULL;
    if (not_valid(self)) return NULL;

    mods = List_to_LDAPMods( modlist, 1 );
    if (mods == NULL)
        return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_add_ext( self->ldap, dn, mods, server_ldcs, client_ldcs, &msgid);
    LDAP_END_ALLOW_THREADS( self );
    LDAPMods_DEL( mods );
    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_add_ext" );

    return PyInt_FromLong(msgid);
}

/* ldap_simple_bind */

static PyObject*
l_ldap_simple_bind( LDAPObject* self, PyObject* args )
{
    char *who;
    int msgid;
    int ldaperror;
    Py_ssize_t cred_len;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;
    struct berval cred;

    if (!PyArg_ParseTuple( args, "ss#|OO", &who, &cred.bv_val, &cred_len, &serverctrls, &clientctrls )) return NULL;
    cred.bv_len = (ber_len_t) cred_len;

    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_sasl_bind( self->ldap, who, LDAP_SASL_SIMPLE, &cred, server_ldcs, client_ldcs, &msgid);
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_simple_bind" );

    return PyInt_FromLong( msgid );
}


#ifdef HAVE_SASL
/* The following functions implement SASL binds. A new method
   sasl_interactive_bind_s(bind_dn, sasl_mechanism) has been introduced.

   * The bind_dn argument will be passed to the c library; however,
     normally it is not needed and should be an empty string.

   * The sasl_mechanism argument is an instance of a class that
     implements a callback interface. For convenience, it should be
     derived from the sasl class (which lives in the ldap.sasl module).
     See the module documentation for more information.

     Check your /usr/lib/sasl/ directory for locally installed SASL
     auth modules ("mechanisms"), or try

       ldapsearch   -b "" -s base -LLL -x  supportedSASLMechanisms
     
     (perhaps with an additional -h and -p argument for ldap host and
     port). The latter will show you which SASL mechanisms are known
     to the LDAP server. If you do not want to set up Kerberos, you
     can still use SASL binds. Your authentication data should then be
     stored in /etc/sasldb (see saslpasswd(8)). If the LDAP server
     does not find the sasldb, it wont allow for DIGEST-MD5 and
     CRAM-MD5. One important thing to get started with sasldb: you
     should first add a dummy user (saslpasswd -c dummy), and this
     will give you some strange error messages. Then delete the dummy
     user (saslpasswd -d dummy), and now you can start adding users to
     your sasldb (again, use the -c switch). Strange, eh?

   * The sasl_mechanism object must implement a method, which will be
     called by the sasl lib several times. The prototype of the
     callback looks like this: callback(id, challenge, prompt,
     defresult) has to return a string (or maybe None). The id
     argument specifies, which information should be passed back to
     the SASL lib (see SASL_CB_xxx in sasl.h)


   A nice "Howto get LDAPv3 up and running with Kerberos and SSL" can
   be found at http://www.bayour.com/LDAPv3-HOWTO.html.  Instead of
   MIT Kerberos, I used Heimdal for my tests (since it is included
   with SuSE Linux).

   Todo:
   
   * Find a better interface than the python callback. This is 
     really ugly. Perhaps one could make use of a sasl class, like
     in the perl ldap module.

   * Thread safety?

   * Memory Management?
   
   * Write more docs

   * ...

*/
static int interaction ( unsigned flags, 
                         sasl_interact_t *interact,
                         PyObject* SASLObject )
{
/*  const char *dflt = interact->defresult; */
  PyObject *result;
  char *c_result;
  result = PyObject_CallMethod(SASLObject,
                               "callback",
                               "isss",
                               interact->id,  /* see sasl.h */
                               interact->challenge,
                               interact->prompt,   
                               interact->defresult);

  if (result == NULL) 
    /*searching for a better error code */
    return LDAP_OPERATIONS_ERROR; 
  c_result = PyString_AsString(result); /*xxx Error checking?? */
  
  /* according to the sasl docs, we should malloc() the returned
     string only for calls where interact->id == SASL_CB_PASS, so we
     probably leak a few bytes per ldap bind. However, if I restrict
     the strdup() to this case, I get segfaults. Should probably be
     fixed sometimes.
  */
  interact->result = strdup( c_result );
  if (interact->result == NULL)
    return LDAP_OPERATIONS_ERROR;
  interact->len = strlen(c_result);
  /* We _should_ overwrite the python string buffer for security
     reasons, however we may not (api/stringObjects.html). Any ideas?
  */
  
  Py_DECREF(result); /*not needed any longer */
  result = NULL;
  
  return LDAP_SUCCESS;
}


/* 
  This function will be called by ldap_sasl_interactive_bind(). The
  "*in" is an array of sasl_interact_t's (see sasl.h for a
  reference). The last interact in the array has an interact->id of
  SASL_CB_LIST_END.

*/

int py_ldap_sasl_interaction(   LDAP *ld, 
                                unsigned flags, 
                                void *defaults,
                                void *in )
{
  /* These are just typecasts */
  sasl_interact_t *interact = (sasl_interact_t *) in;
  PyObject *SASLObject = (PyObject *) defaults;
  /* Loop over the array of sasl_interact_t structs */
  while( interact->id != SASL_CB_LIST_END ) {
    int rc = 0;
    rc = interaction( flags, interact, SASLObject );
    if( rc )  return rc;
    interact++;
  }
  return LDAP_SUCCESS;
}

static PyObject* 
l_ldap_sasl_interactive_bind_s( LDAPObject* self, PyObject* args )
{
    char *c_mechanism;
    char *who;

    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    PyObject       *SASLObject = NULL;
    PyObject *mechanism = NULL;
    int msgid;

    static unsigned sasl_flags = LDAP_SASL_QUIET;

    /* 
     * In Python 2.3+, a "I" format argument indicates that we're either converting
     * the Python object into a long or an unsigned int. In versions prior to that,
     * it will always convert to a long. Since the sasl_flags variable is an
     * unsigned int, we need to use the "I" flag if we're running Python 2.3+ and a
     * "i" otherwise. 
     */
#if (PY_MAJOR_VERSION == 2) && (PY_MINOR_VERSION < 3)
    if (!PyArg_ParseTuple(args, "sOOOi", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags ))
#else
    if (!PyArg_ParseTuple(args, "sOOOI", &who, &SASLObject, &serverctrls, &clientctrls, &sasl_flags ))
#endif
      return NULL;

    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    /* now we extract the sasl mechanism from the SASL Object */
    mechanism = PyObject_GetAttrString(SASLObject, "mech");
    if (mechanism == NULL) return NULL;
    c_mechanism = PyString_AsString(mechanism);
    Py_DECREF(mechanism);
    mechanism = NULL;

    /* Don't know if it is the "intended use" of the defaults
       parameter of ldap_sasl_interactive_bind_s when we pass the
       Python object SASLObject, but passing it through some
       static variable would destroy thread safety, IMHO.
     */
    msgid = ldap_sasl_interactive_bind_s(self->ldap, 
                                         who, 
                                         c_mechanism, 
                                         (LDAPControl**) server_ldcs, 
                                         (LDAPControl**) client_ldcs,
                                         sasl_flags, 
                                         py_ldap_sasl_interaction, 
                                         SASLObject);

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if (msgid != LDAP_SUCCESS)
        return LDAPerror( self->ldap, "ldap_sasl_interactive_bind_s" );
    return PyInt_FromLong( msgid );
}
#endif


#ifdef LDAP_API_FEATURE_CANCEL

/* ldap_cancel */

static PyObject*
l_ldap_cancel( LDAPObject* self, PyObject* args )
{
    int msgid;
    int cancelid;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int ldaperror;

    if (!PyArg_ParseTuple( args, "i|OO", &cancelid, &serverctrls, &clientctrls)) return NULL;
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_cancel( self->ldap, cancelid, server_ldcs, client_ldcs, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_cancel" );

    return PyInt_FromLong( msgid );
}

#endif

/* ldap_compare_ext */

static PyObject *
l_ldap_compare_ext( LDAPObject* self, PyObject *args )
{
    char *dn, *attr;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;
    Py_ssize_t value_len;
    struct berval value;

    if (!PyArg_ParseTuple( args, "sss#|OO", &dn, &attr, &value.bv_val, &value_len, &serverctrls, &clientctrls )) return NULL;
    value.bv_len = (ber_len_t) value_len;

    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_compare_ext( self->ldap, dn, attr, &value, server_ldcs, client_ldcs, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_compare_ext" );

    return PyInt_FromLong( msgid );
}


/* ldap_delete_ext */

static PyObject *
l_ldap_delete_ext( LDAPObject* self, PyObject *args )
{
    char *dn;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;

    if (!PyArg_ParseTuple( args, "s|OO", &dn, &serverctrls, &clientctrls )) return NULL;
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_delete_ext( self->ldap, dn, server_ldcs, client_ldcs, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_delete_ext" );

    return PyInt_FromLong(msgid);
}


/* ldap_modify_ext */

static PyObject *
l_ldap_modify_ext( LDAPObject* self, PyObject *args )
{
    char *dn;
    PyObject *modlist;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;
    LDAPMod **mods;

    if (!PyArg_ParseTuple( args, "sO|OO", &dn, &modlist, &serverctrls, &clientctrls )) return NULL;
    if (not_valid(self)) return NULL;

    mods = List_to_LDAPMods( modlist, 0 );
    if (mods == NULL)
        return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_modify_ext( self->ldap, dn, mods, server_ldcs, client_ldcs, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    LDAPMods_DEL( mods );
    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_modify_ext" );

    return PyInt_FromLong( msgid );
}


/* ldap_rename */

static PyObject *
l_ldap_rename( LDAPObject* self, PyObject *args )
{
    char *dn, *newrdn;
    char *newSuperior = NULL;
    int delold = 1;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;

    if (!PyArg_ParseTuple( args, "ss|ziOO", &dn, &newrdn, &newSuperior, &delold, &serverctrls, &clientctrls ))
        return NULL;
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_rename( self->ldap, dn, newrdn, newSuperior, delold, server_ldcs, client_ldcs, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_rename" );

    return PyInt_FromLong( msgid );
}


/* ldap_result4 */

static PyObject *
l_ldap_result4( LDAPObject* self, PyObject *args )
{
    int msgid = LDAP_RES_ANY;
    int all = 1;
    double timeout = -1.0;
    int add_ctrls = 0;
    int add_intermediates = 0;
    int add_extop = 0;
    struct timeval tv;
    struct timeval* tvp;
    int res_type;
    LDAPMessage *msg = NULL;
    PyObject *result_str, *retval, *pmsg, *pyctrls = 0;
    int res_msgid = 0;
    char *retoid = 0;
    PyObject *valuestr = 0;

    if (!PyArg_ParseTuple( args, "|iidiii", &msgid, &all, &timeout, &add_ctrls, &add_intermediates, &add_extop ))
        return NULL;
    if (not_valid(self)) return NULL;
    
    if (timeout >= 0) {
        tvp = &tv;
        set_timeval_from_double( tvp, timeout );
    } else {
        tvp = NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    res_type = ldap_result( self->ldap, msgid, all, tvp, &msg );
    LDAP_END_ALLOW_THREADS( self );

    if (res_type < 0)   /* LDAP or system error */
        return LDAPerror( self->ldap, "ldap_result4" );

    if (res_type == 0) {
        /* Polls return (None, None, None, None); timeouts raise an exception */
        if (timeout == 0) {
            if (add_extop) {
                return Py_BuildValue("(OOOOOO)", Py_None, Py_None, Py_None, Py_None, Py_None, Py_None);
            } else {
                return Py_BuildValue("(OOOO)", Py_None, Py_None, Py_None, Py_None);
            }
        }
        else
            return LDAPerr(LDAP_TIMEOUT);
    }

    if (msg)
        res_msgid = ldap_msgid(msg);

    int result = LDAP_SUCCESS;
    char **refs = NULL;
    LDAPControl **serverctrls = 0;
    LDAP_BEGIN_ALLOW_THREADS( self );
    if (res_type == LDAP_RES_SEARCH_ENTRY) {
        /* LDAPmessage_to_python will parse entries and read the controls for each entry */
    } else if (res_type == LDAP_RES_SEARCH_REFERENCE) {
        /* LDAPmessage_to_python will parse refs and read the controls for each res */
    } else if (res_type == LDAP_RES_INTERMEDIATE) {
        /* LDAPmessage_to_python will parse intermediates and controls */
    } else {
        int rc;
        if (res_type == LDAP_RES_EXTENDED) {
            struct berval *retdata = 0;
            rc = ldap_parse_extended_result( self->ldap, msg, &retoid, &retdata, 0 );
            /* handle error rc!=0 here? */
            if (rc == LDAP_SUCCESS) {
                valuestr = LDAPberval_to_object(retdata);
            }
            ber_bvfree( retdata );
        }
            
        rc = ldap_parse_result( self->ldap, msg, &result, NULL, NULL, &refs,
                                &serverctrls, 0 );
    }
    LDAP_END_ALLOW_THREADS( self );

    if (result != LDAP_SUCCESS) {               /* result error */
        char *e, err[1024];
        if (result == LDAP_REFERRAL && refs && refs[0]) {
            snprintf(err, sizeof(err), "Referral:\n%s", refs[0]);
            e = err;
        } else
            e = "ldap_parse_result";
        ldap_msgfree(msg);
        return LDAPerror( self->ldap, e );
    }

    if (!(pyctrls = LDAPControls_to_List(serverctrls))) {
        int err = LDAP_NO_MEMORY;
        ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &err);
        ldap_msgfree(msg);
        return LDAPerror(self->ldap, "LDAPControls_to_List");
    }
    ldap_controls_free(serverctrls);

    pmsg = LDAPmessage_to_python( self->ldap, msg, add_ctrls, add_intermediates );

    result_str = LDAPconstant( res_type );

    if (pmsg == NULL) {
            retval = NULL;
    } else {
        /* s handles NULL, but O does not */
        if (add_extop) {
            retval = Py_BuildValue("(OOiOsO)", result_str, pmsg, res_msgid,
                                   pyctrls, retoid, valuestr ? valuestr : Py_None);
        } else {
            retval = Py_BuildValue("(OOiO)", result_str, pmsg, res_msgid, pyctrls);
        }

        if (pmsg != Py_None) {
        Py_DECREF(pmsg);
        }
    }
    if (valuestr) {
        Py_DECREF(valuestr);
    }
    Py_XDECREF(pyctrls);
    Py_DECREF(result_str);
    return retval;
}


/* ldap_search_ext */

static PyObject*
l_ldap_search_ext( LDAPObject* self, PyObject* args )
{
    char *base;
    int scope;
    char *filter;
    PyObject *attrlist = Py_None;
    char **attrs;
    int attrsonly = 0;

    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    double timeout = -1.0;
    struct timeval tv;
    struct timeval* tvp;

    int sizelimit = 0;

    int msgid;
    int ldaperror;

    if (!PyArg_ParseTuple( args, "sis|OiOOdi",
                           &base, &scope, &filter, &attrlist, &attrsonly,
                           &serverctrls, &clientctrls, &timeout, &sizelimit )) return NULL;
    if (not_valid(self)) return NULL;

    if (!attrs_from_List( attrlist, &attrs )) 
         return NULL;

    if (timeout >= 0) {
        tvp = &tv;
        set_timeval_from_double( tvp, timeout );
    } else {
        tvp = NULL;
    }

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_search_ext( self->ldap, base, scope, filter, attrs, attrsonly,
                             server_ldcs, client_ldcs, tvp, sizelimit, &msgid );
    LDAP_END_ALLOW_THREADS( self );

    free_attrs( &attrs );
    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_search_ext" );

    return PyInt_FromLong( msgid );
}       


/* ldap_whoami_s (available since OpenLDAP 2.1.13) */

static PyObject*
l_ldap_whoami_s( LDAPObject* self, PyObject* args )
{
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    struct berval *bvalue = NULL;

    PyObject *result;

    int ldaperror;

    if (!PyArg_ParseTuple( args, "|OO", &serverctrls, &clientctrls)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_whoami_s( self->ldap, &bvalue, server_ldcs, client_ldcs );
    LDAP_END_ALLOW_THREADS( self );

    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_whoami_s" );

    result = LDAPberval_to_object(bvalue);

    return result;
}

#ifdef HAVE_TLS
/* ldap_start_tls_s */

static PyObject*
l_ldap_start_tls_s( LDAPObject* self, PyObject* args )
{
    int result;

    if (!PyArg_ParseTuple( args, "" )) return NULL;
    if (not_valid(self)) return NULL;

    result = ldap_start_tls_s( self->ldap, NULL, NULL );
    if ( result != LDAP_SUCCESS ){
        ldap_set_option(self->ldap, LDAP_OPT_ERROR_NUMBER, &result);
        return LDAPerror( self->ldap, "ldap_start_tls_s" );
    }

    Py_INCREF(Py_None);
    return Py_None;
}

#endif

/* ldap_set_option */

static PyObject*
l_ldap_set_option(PyObject* self, PyObject *args)
{
    PyObject *value;
    int option;

    if (!PyArg_ParseTuple(args, "iO:set_option", &option, &value))
        return NULL;
    if (LDAP_set_option((LDAPObject *)self, option, value) == -1)
        return NULL;
    Py_INCREF(Py_None);
    return Py_None;
}


/* ldap_get_option */

static PyObject*
l_ldap_get_option(PyObject* self, PyObject *args)
{
    int option;

    if (!PyArg_ParseTuple(args, "i:get_option", &option))
        return NULL;
    return LDAP_get_option((LDAPObject *)self, option);
}


/* ldap_passwd */

static PyObject *
l_ldap_passwd( LDAPObject* self, PyObject *args )
{
    struct berval user;
    Py_ssize_t user_len;
    struct berval oldpw;
    Py_ssize_t oldpw_len;
    struct berval newpw;
    Py_ssize_t newpw_len;
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;

    if (!PyArg_ParseTuple( args, "z#z#z#|OO", &user.bv_val, &user_len, &oldpw.bv_val, &oldpw_len, &newpw.bv_val, &newpw_len, &serverctrls, &clientctrls ))
        return NULL;

    user.bv_len = (ber_len_t) user_len;
    oldpw.bv_len = (ber_len_t) oldpw_len;
    newpw.bv_len = (ber_len_t) newpw_len;
    
    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_passwd( self->ldap,
            user.bv_val != NULL ? &user : NULL,
            oldpw.bv_val != NULL ? &oldpw : NULL,
            newpw.bv_val != NULL ? &newpw : NULL,
            server_ldcs,
            client_ldcs,
            &msgid );
    LDAP_END_ALLOW_THREADS( self );
    
    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_passwd" );

    return PyInt_FromLong( msgid );
}


/* ldap_extended_operation */

static PyObject *
l_ldap_extended_operation( LDAPObject* self, PyObject *args )
{
    char *reqoid = NULL;
    struct berval reqvalue = {0, NULL};
    PyObject *serverctrls = Py_None;
    PyObject *clientctrls = Py_None;
    LDAPControl** server_ldcs = NULL;
    LDAPControl** client_ldcs = NULL;

    int msgid;
    int ldaperror;

    if (!PyArg_ParseTuple( args, "sz#|OO", &reqoid, &reqvalue.bv_val, &reqvalue.bv_len, &serverctrls, &clientctrls ))
        return NULL;

    if (not_valid(self)) return NULL;

    if (!PyNone_Check(serverctrls)) {
        if (!LDAPControls_from_object(serverctrls, &server_ldcs))
            return NULL;
    }

    if (!PyNone_Check(clientctrls)) {
        if (!LDAPControls_from_object(clientctrls, &client_ldcs))
            return NULL;
    }

    LDAP_BEGIN_ALLOW_THREADS( self );
    ldaperror = ldap_extended_operation( self->ldap, reqoid,
            reqvalue.bv_val != NULL ? &reqvalue : NULL,
            server_ldcs,
            client_ldcs,
            &msgid );
    LDAP_END_ALLOW_THREADS( self );
    
    LDAPControl_List_DEL( server_ldcs );
    LDAPControl_List_DEL( client_ldcs );

    if ( ldaperror!=LDAP_SUCCESS )
        return LDAPerror( self->ldap, "ldap_extended_operation" );

    return PyInt_FromLong( msgid );
}

/* methods */

static PyMethodDef methods[] = {
    {"unbind_ext",      (PyCFunction)l_ldap_unbind_ext,         METH_VARARGS },
    {"abandon_ext",     (PyCFunction)l_ldap_abandon_ext,        METH_VARARGS },
    {"add_ext",         (PyCFunction)l_ldap_add_ext,            METH_VARARGS },
    {"simple_bind",     (PyCFunction)l_ldap_simple_bind,        METH_VARARGS },
#ifdef HAVE_SASL
    {"sasl_interactive_bind_s", (PyCFunction)l_ldap_sasl_interactive_bind_s,    METH_VARARGS },
#endif
    {"compare_ext",     (PyCFunction)l_ldap_compare_ext,        METH_VARARGS },
    {"delete_ext",      (PyCFunction)l_ldap_delete_ext,         METH_VARARGS },
    {"modify_ext",      (PyCFunction)l_ldap_modify_ext,         METH_VARARGS },
    {"rename",          (PyCFunction)l_ldap_rename,             METH_VARARGS },
    {"result4",         (PyCFunction)l_ldap_result4,            METH_VARARGS },
    {"search_ext",      (PyCFunction)l_ldap_search_ext,         METH_VARARGS },
#ifdef HAVE_TLS
    {"start_tls_s",     (PyCFunction)l_ldap_start_tls_s,        METH_VARARGS },
#endif
    {"whoami_s",        (PyCFunction)l_ldap_whoami_s,           METH_VARARGS },
    {"passwd",          (PyCFunction)l_ldap_passwd,             METH_VARARGS },
    {"set_option",      (PyCFunction)l_ldap_set_option,         METH_VARARGS },
    {"get_option",      (PyCFunction)l_ldap_get_option,         METH_VARARGS },
#ifdef LDAP_API_FEATURE_CANCEL
    {"cancel",          (PyCFunction)l_ldap_cancel,             METH_VARARGS },
#endif
    {"extop",           (PyCFunction)l_ldap_extended_operation,         METH_VARARGS },
    { NULL, NULL }
};

/* get attribute */

static PyObject*
getattr(LDAPObject* self, char* name) 
{
        return Py_FindMethod(methods, (PyObject*)self, name);
}

/* set attribute */

static int
setattr(LDAPObject* self, char* name, PyObject* value) 
{
        PyErr_SetString(PyExc_AttributeError, name);
        return -1;
}

/* type entry */

PyTypeObject LDAP_Type = {
#if defined(MS_WINDOWS) || defined(__CYGWIN__)
        /* see http://www.python.org/doc/FAQ.html#3.24 */
        PyObject_HEAD_INIT(NULL)
#else /* ! MS_WINDOWS */
        PyObject_HEAD_INIT(&PyType_Type)
#endif /* MS_WINDOWS */
        0,                      /*ob_size*/
        "LDAP",                 /*tp_name*/
        sizeof(LDAPObject),     /*tp_basicsize*/
        0,                      /*tp_itemsize*/
        /* methods */
        (destructor)dealloc,    /*tp_dealloc*/
        0,                      /*tp_print*/
        (getattrfunc)getattr,   /*tp_getattr*/
        (setattrfunc)setattr,   /*tp_setattr*/
        0,                      /*tp_compare*/
        0,                      /*tp_repr*/
        0,                      /*tp_as_number*/
        0,                      /*tp_as_sequence*/
        0,                      /*tp_as_mapping*/
        0,                      /*tp_hash*/
};
