Hi,

I've been working on implementing an RFC 4533 syncrepl consumer using python-ldap.  I can't work out why I can't get the SyncDoneControl that is returned with the LDAP_RES_SEARCH_RESULT through python-ldap.  The SyncDoneControl contains the cookie that is needed for stateful/minimal syncrepl refresh only, so losing that control makes the code a lot less useful.

It looks like SyncStateControl is never returned to the Python code because Modules/LDAPObject.c l_ldap_result3() calls LDAPmessage_to_python() for LDAP_RES_SEARCH_ENTRY.  LDAPmessage_to_python() discards controls in the result.  I can live with this.

I'm mystified as to why Modules/LDAPObject.c l_ldap_result3() doesn't return the SyncDoneControl with the LDAP_RES_SEARCH_RESULT.  I've waded through the source and I'm pretty sure that nothing is deliberately filtering out the control.

Here we can see that the control is in the result when l_ldap_result3() calls ldap_parse_result():

Breakpoint 2, ldap_parse_result (ld=0x783710, r=0x730830,
    errcodep=0x7fff5d8582c4, matcheddnp=0x0, errmsgp=0x0,
    referralsp=0x7fff5d8582a8, serverctrls=0x7fff5d8582a0, freeit=0)
    at error.c:261
(gdb) p *r->lm_ber
$6 = {ber_opts = {lbo_valid = 2, lbo_options = 1, lbo_debug = 0},
  ber_tag = 121, ber_len = 81, ber_usertag = 0,
  ber_buf = 0x7e3d60 "\002\001\001yL\200\0301.3.6.1.4.1.4203.1.9.1.4\2010˘.\004,csn=20090317201420Z#000000#00#000000,rid=000",
  ber_ptr = 0x7e3d63 "yL\200\0301.3.6.1.4.1.4203.1.9.1.4\2010˘.\004,csn=20090317201420Z#000000#00#000000,rid=000", ber_end = 0x7e3db1 "", ber_sos = 0x0,
  ber_rwptr = 0x0, ber_memctx = 0x0}
(gdb) p *serverctrls
$7 = (LDAPControl **) 0x0

Here is my sample code:

class SyncRequestControl(ldap.controls.LDAPControl):
  # The Sync Request Control is an LDAP Control [RFC4511] where the
  # controlType is the object identifier 1.3.6.1.4.1.4203.1.9.1.1 and the
  # controlValue, an OCTET STRING, contains a BER-encoded
  # syncRequestValue.  The criticality field is either TRUE or FALSE.
  #   syncRequestValue ::= SEQUENCE {
  #     mode ENUMERATED {
  #                      -- 0 unused
  #                      refreshOnly       (1),
  #                      -- 2 reserved
  #                      refreshAndPersist (3)
  #     },
  #     cookie     syncCookie OPTIONAL,
  #     reloadHint BOOLEAN DEFAULT FALSE
  #   }
  # The Sync Request Control is only applicable to the SearchRequest Message.

  controlType='1.3.6.1.4.1.4203.1.9.1.1'

  def __init__(self, controlType='1.3.6.1.4.1.4203.1.9.1.1', criticality=False,
               controlValue=None, encodedControlValue=None):
    ldap.controls.LDAPControl.__init__(self, self.controlType, criticality,
                                       controlValue, encodedControlValue)

  def encodeControlValue(self, value):
    # 30 31          Sequence tag (len=0x31)
    #       0a 01 01 Enumerated (len=0x01) value 0x01 (RefreshOnly)
    #       04 2C    Octet String (len=0x2C) value 'csn=...,rid=000'
    #                63 73 6e 3d 32 30 30 39 30 33 31 36 31 39 35 35
    #                30 39 5a 23 30 30 30 30 30 30 23 30 30 23 30 30
    #                30 30 30 30 2c 72 69 64 3d 30 30 30

    mode, cookie, reload_hint = value

    # Enumerated (len=0x01) value 0x01 (RefreshOnly)
    result_content = struct.pack('BBB', 0x0A, 0x01, mode)

    # Octet String (optional)
    if cookie:
      result_content += struct.pack('BB', 0x04, len(cookie))
      result_content += cookie

    # Boolean (optional)
    if reload_hint:
      result_content += struct.pack('BBB', 0x01, 0x01, 0xFF)

    # Sequence tag
    result_header = struct.pack('BB', 0x30, len(result_content))

    return result_header + result_content

ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
ldap.set_option(ldap.OPT_REFERRALS, 0)
conn = ldap.initialize(host)
sync_req_ctrl=SyncRequestControl(criticality=True,
                                 controlValue=(3, cookie, True))

op_id = conn.search_ext(base, ldap.SCOPE_ONELEVEL,
                        attrlist=('dn',),
                        serverctrls=(sync_req_ctrl,),
                        )

# ldap.controls.knownLDAPControls[SyncStateControl.controlType] = SyncStateControl
# ldap.controls.knownLDAPControls[SyncDoneControl.controlType] = SyncDoneControl

#rtype, rdata, rmsgid, decoded_serverctrl = conn.result3(all=0, timeout=60)
rtype, rdata, rmsgid, serverctrls = conn._ldap_call(conn._l.result3,_ldap.RES_ANY,0,60)
while rtype:
  print 'rtype=%s' % rtype
  print 'rdata=%s' % rdata
  print 'rmsgid=%s' % rmsgid
  print 'serverctrls=%s' % serverctrls
  print ''
  # rtype, rdata, rmsgid, decoded_serverctrl = conn.result3(all=0, timeout=10)
  rtype, rdata, rmsgid, serverctrls = conn._ldap_call(conn._l.result3,_ldap.RES_ANY,0,10)

--
Thanks,
Sean Burford