Examining Microsoft's LDAP API
Programmer's Notes
by Sven B. Schreiber


Editor's Note: This document supplements the article "Examining Microsoft's LDAP API," by Sven B. Schreiber, in the December 1998 issue of Dr. Dobb's Journal. 

ASN.1 and BER
Flipping through the pages of RFC 1777 or 2251, you'll see a very special syntax notation used throughout the document. In section 4 of RFC 2251 (p. 9), you learn that this is the "Abstract Syntax Notation One" (ASN.1) defined in Recommendation X.680 of the International Telecommunications Union (ITU). RFC 1777 references this document as CCITT Recommendation X.208, which is nothing but an outdated predecessor of X.680.

ASN.1 is a clever instrument to denote arbitrary data types of high complexity independent of programming language. Since a networking protocol is basically a rule-based exchange of command and data packets between two network stations, ASN.1 is suited quite well for the structural specification of those packets. However, ASN.1 is just a grammar to aid writing down the formal aspects of a protocol. The actual implementation is a different story. To provide a clean mapping between the abstract formalism and the implementation, some encoding rules are necessary. This is the topic of yet another ITU Recommendation named X.690.

Contrary to RFCs, ITU papers are not generally available for free. If you're lucky, you'll find some of them on one of the many "Standards" compilation CDs at a reasonable price. Otherwise, you can order them electronically from the web site of the ITU. A good starting point for the "X Files" X.500 and up is found inside the "Telecommunication Standardization Sector" at http://www.itu.ch/itudoc/itu-t/rec/x/x500up.html. Another series X page with download hyperlinks is located at https://ecs.itu.ch/itudoc/itu-t/rec/x/bookshop.html. This is the ITU's Electronic Bookshop. However, the former URL seems to contain more recent and other related material (corrigenda, summaries, etc.) than the latter. Try both of them and decide which one better fits your needs.

Document prices range from 20 to about 100 Swiss Francs (CHF). The typical price for a paper of around 50 pages amounts to 20 CHF, papers in the 100 page category usually cost 40 CHF. Payments by credit card are accepted, and after you have filled out an online form with your personal data and the card information, you are directed immediately to a download page where you can obtain the text in form of a WinWord or PostScript file. Please note that I had considerable problems printing out the WinWord version, so I recommend using the PostScript file whenever possible. If you don't have a PostScript printer, the Win32 versions of GhostScript 5.10 and GSview 2.5 (http://www.cs.wisc.edu/~ghost/index.html) will do a wonderful job.

Don't let you be confused by the fact that some RFCs refer to X.680 and X.690 by a completely different name, using ISO/IEC document numbers. (ISO = International Organization for Standardization, IEC = International Electrotechnical Commission.) For many ITU Recommendations, an identical ISO/IEC document exists. For instance, X.680 corresponds to ISO/IEC 8824-1, and X.690 to ISO/IEC 8825-1. The ISO runs its own web site in Switzerland (http://www.iso.ch/). However, I found that getting the papers from the ITU is much easier.

X.690 - fully titled "Information Technology - ASN.1 Encoding Rules: Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER)" - is a lengthy paper, explaining how any ASN.1 item should preferably be encoded and sent across a communication media. As RFC 2251 states in section 5.1., it's the "Basic Encoding Rules" (BER) that are used to encode all LDAP protocol elements. BER-encoded LDAP packets are carried from point to point inside TCP packets.

BER are not the easiest way to implement a protocol. BER make heavy use of tagged data blocks with length/data pairs, while attempting to minimize the amount of bytes to be transmitted. For instance, integers have their own tag byte value and are preceded by a length byte, so leading zeroes are not transmitted. If you're examining an LDAP data stream running through an LDAP port by tracing WinSock send() and recv() calls, you won't see many familiar patterns on first sight, except for some text strings scattered between binary data. It's only after thorough examination of the tag and length bytes that the underlying structure emerges.

Consider the snippet from RFC 2251, shown in Example 1 below. This is the ASN.1 definition of an LDAP request packet named BindRequest used to log into a server, which consists of a tag byte of value [APPLICATION 0] (see section 28.1 of X.680), followed by a three-part SEQUENCE type. A SEQUENCE is roughly equivalent to a C structure, so the three parts of a BindRequest correspond to structure members. The first member is an INTEGER - not a simple C-style integer, but a BER-encoded quantity, consisting of a another tag byte, a length indicator, and a value field. The second member is of type LDAPDN, which is defined somewhere else as a synonym for OCTET STRING, i.e. a text item (again, consisting of a tag-length-data triplet).

The last member is another complex data type named "AuthenticationCode", defined in Example 2 below. The CHOICE type is similar to a C union, i.e. its members can appear alternatively. However, in contrast to a union, the length of a CHOICE item is not determined by the longest member, but is simply the length of the member actually present. Here, we see two potential authentication methods, tagged by numbers 0 or 3, where 0 is followed by a text field, and 3 by yet another complex data type. This game can be taken further ad infinitum (well, almost).

Authentication
Binding to an LDAP server means authentication. This is typically a server-specific operation, so the LDAP binding APIs come in several flavors. The most basic among them is the ldap_simple_bind() function that takes a connection handle, an account name, and a clear-text password. Using this API is obviously not a good choice, since the password will be transferred to the server in an LDAP protocol data unit (PDU) without any encryption. This method should be regarded as a last resort for systems that do not support any of the more secure authentication mechanisms.

LDAPv2 offered two alternative binding APIs: ldap_kerberos_bind(), and the general purpose function ldap_bind(). Both of them are obsoleted by the new LDAPv3 function ldap_sasl_bind(). SASL is the acronym for "Simple Authentication and Security Layer", which is specified in RFC 2222 written by J. Myers of Netscape Communications. Note that neither ldap_kerberos_bind() nor ldap_sasl_bind() are supported by wldap32.dll version 5.0.1515.0, so ldap_bind() is the only alternative to ldap_simple_bind() to date.

Many of the LDAP APIs come in two or even three flavors, most notably those that involve remote operations potentially taking considerable time. If a function name has an "_s" postfix attached to it, you know that this function is executed synchronously, i.e. it returns an LDAP status code and blocks until done. In this case, you can bet that a synchronous version without the "_s" appendix exists as well, returning some kind of a handle that can be used in ldap_result() or ldap_abandon() calls to poll or abort a pending operation, respectively. Some synchronous functions have an additional sub-variant ending in "_st", which takes an additional timeout parameter.

If you want to make use of the NT security mechanisms, ldap_bind_s() is the best choice. This synchronous variant of ldap_bind() requires a connection handle, an account name, a pointer to a sequence of bytes referred to as "credentials", and a method parameter indicating which authentication method should be used. RFC 1823 defines the methods LDAP_AUTH_SIMPLE, LDAP_AUTH_KRBV41, and LDAP_AUTH_KRBV42. The first method is "Simple Authentication" and corresponds to ldap_simple_bind*(). In this case, the clear-text password serves as the "credentials".

The other two methods - Kerberos V4.1 and V4.2 - are not defined in winldap.h and thus aren't available under Win32. However, winldap.h introduces several other authentication codes neither mentioned in RFC 1823 nor in the Internet Draft of the forthcoming LDAPv3 API (draft-ietf-asid-ldap-c-api-00.txt). Out of this set, LDAP_AUTH_NTLM is the most interesting one: NTLM probably means "NT LAN Manager" and is referred to as "Windows NT challenge/response" in Microsoft's "Exchange Server Administrator" tool. Using LDAP_AUTH_NTLM enables you to reuse the account currently logged into the workstation for the Exchange Server login. Please note that this method doesn't require any credentials, so the corresponding ldap_bind*() argument is ignored.

Like all names passed to LDAP APIs, the account name used in calls to ldap_bind*() must be a "Distinguished Name". This term is explained in more detail below. Here, it should suffice to say that an account name is either formatted as a single "Common Name" (cn) like "cn=<name>", where <name> is a valid user name in the NT domain of the Exchange Server, or as a combination of a domain and common name, like "dc=<domain>,cn=<name>". The demo DLL coming with this article contains a handy API that converts an account name from the standard NT notation (i.e. <name> or <domain>\<name>) to the format required in the LDAP bind request.

As soon as the client has performed all operations on the server's directory, it disconnects and releases the connection handle by calling ldap_unbind() or ldap_unbind_s(). After reading the previous paragraph, you might assume that ldap_unbind() works asynchronously. Well, this function is one of the few exceptions to the rule. ldap_unbind() has originally been defined as synchronous in RFC 1823 and has remained so in LDAPv3, even though ldap_unbind_s() was added. Besides compatibility considerations, the main reason for this oddity is probably that the LDAP UnbindRequest defined in section 4.3. of RFC 2251 has no corresponding server response - therefore, defining an asynchronous unbinding function would be pure nonsense.

The X.500 Directory
In the preceding sections, I've frequently mentioned the "directory" of a server. So what's a directory in the first place? Basically, it's a hierarchically organized structure of objects exposed by the server. Typical server objects are: Other servers participating in a site, available protocols, installed mailboxes, and the like. Those objects form a tree structure that can be walked along, much like a file directory browser does. This tree is called the Directory Information Tree (DIT).

The theoretical basis of the Exchange Server directory is ITU recommendation X.500, titled "Information Technology - Open Systems Interconnection - The Directory: Overview of Concepts, Models, and Services". This 28-page document is just a small part of the story - all ITU recommendations in the number range 500-599 are related to "The Directory". Currently, this set comprises X.501, X.509, X.511, X.518, X.519, X.520, X.521, X.525, X.581, and X.582. X.519 is a quite interesting one, since it defines the predecessor of LDAP - the Directory Access Protocol (DAP). As explained in RFC 1777, one of the major efforts in designing LDAP was to avoid the high resource requirements introduced by DAP.

Every node in the DIT is assigned a "Distinguished Name" (DN) that identifies this node uniquely. The DN of any node consists of an individual "Relative Distinguished Name" (RDN), followed by the DN of its parent node. The notation used is similar to the X.400 labeled address format used by message handling services to identify message originators or recipients (see ITU Recommendation F.401 for details). A DN is constructed by concatenating several <type>=<value> pairs, separated by commas. For example, the DN of my Exchange mailbox on my test server is "cn=Sven B. Schreiber,ou=Recipients,ou=SBS,o=SBS". "cn" is a "Common Name", "ou" denotes an "Organizational Unit", and "o" represents the organization (i.e. the Exchange site name, in this case).

In section 8 of X.500, the "Directory Service" is specified. This is the set of services provided by the directory in response to client requests. Therefore, this section has the most notable impact on the structure of the LDAP specification, as well as the LDAP client API. For instance, subsection 8.3 ("Directory Interrogation") defines the operations "Read", "Compare", "List", "Search", and "Abandon". Subsection 8.4 ("Directory Modification") introduces some more operations, like "Add Entry", "Remove Entry", "Modify Entry", and "Modify Distinguished Name". Later in this article, we'll make heavy use of the "Search" and "List" requests, as well as one of the "Service Controls" introduced in X.500 subsection 8.2.1.

Browsing the Directory
Retrieving the complete DIT of a server is achieved by several calls of a single LDAP API function: ldap_search(), or one of its siblings, ldap_search_s() and ldap_search_st(). Example 3 below outlines the prototype of latter. The first argument, "ld", is an LDAP connection handle - i.e. a pointer to an LDAP-type structure returned by ldap_init() or ldap_open(). The "base" parameter is a string specifying the DN of a directory node serving as the starting point of the search. An empty string denotes the top-level entry.

The "scope" can take on the values LDAP_SCOPE_BASE, LDAP_SCOPE_ONELEVEL, or LDAP_SCOPE_SUBTREE. A directory browser will typically use LDAP_SCOPE_ONELEVEL to descend the hierarchy of nodes downwards. LDAP_SCOPE_BASE is needed to extract the attributes of a given node, most notably the top-level entry, which holds important global information on the directory and the server.

The argument named "filter" is a string that is matched against the attributes of available nodes found in the specified scope. The format of a filter string is defined in RFC 1558. The most frequently used incarnation is the string "(objectClass=*)", which matches all entries that have any value attached to the attribute "objectClass". Since this is a required attribute for all directory entries, this special filter will always match.

The "attrs" array consists of a series of attribute names that indicates which attribute/value pairs should be returned for every match. The last array member must be a NULL pointer. If all attributes should be returned, "attrs" should be NULL. This parameter has serious impact on the performance of a search request, because it controls how much data is transferred across the wire. On low-speed connections, passing in NULL when just a few attributes are to be inspected is obviously not a good idea.

The next argument, "attrsonly", can affect performance in a similar way. If this flag is set, only the attribute names are returned on every hit, and the values are stripped. Since some attribute values - especially those used by Exchange Server 5.0 - might be of considerable length, ignoring values during searching and retrieving individual values only when needed can greatly improve the responsiveness of a directory browser.

A search timeout interval can be specified by setting the tv_sec and tv_usec members of a timeval structure appropriately and passing a pointer to it to ldap_search_st() as the seventh argument, "timeout". ldap_search_st() differs from ldap_search_s() in this respect only. Be aware that the LDAP timeval structure is actually called l_timeval in winldap.h, since the Windows Socket API already defines a timeval structure in winsock.h for the select() function. To avoid any ambiguities, I recommend to always use the LDAP_TIMEVAL alias of this data type.

Finally, there's the "res" parameter. "res" means "result" and is defined as a pointer to a variable that will receive a pointer to an opaque LDAPMessage structure. It's up to the LDAP API implementor to decide what this data type looks like, so no assumptions should be made on its interiors, even though you can look up its definition in winldap.h. Especially, never assume that the implementation of LDAPMessage corresponds to the "Message Envelope" definition of the same name found in section 4.1.1. of RFC 2251. wldap32.dll uses this structure for quick access to the BER-encoded LDAP server response and to keep track of the status of response parsing. It's allocated by the system and must be freed using ldap_msgfree() as soon as it's not needed anymore.

Parsing a Response
To parse a search response received from the server, RFC 1823 defines several APIs, the most common of which are ldap_count_entries(), ldap_first_entry(), and ldap_next_entry(). Scanning the response data for entries is similar to enumerating files in a file directory using the Win32 APIs FindFirstFile() and FindNextFile().

Example 4 below shows that the prototypes of ldap_first_entry() and ldap_next_entry() are quite similar: Both receive a connection handle and an LDAPMessage pointer, and return another LDAPMessage pointer. The only difference is that ldap_first_entry() expects an LDAPMessage result from a previous search request, while ldap_next_entry() must be fed with an ldap_*_entry() result. This enables a client to step sequentially through the hit list of a search response.

The LDAP API introduces several operations that can be performed on DIT entries as returned by the parsing functions:

- ldap_get_dn() returns the "Distinguished Name" (DN) of the entry, or NULL, if an error occurs. As explained above, a DN is a sequence of <type>=<value> pairs separated by commas.
- ldap_dn2ufn() is supposed to convert a DN - which is a formal node address - to a "User Friendly Name" (UFN) according to RFC 1781. However, I found that the UFNs generated by wldap32.dll are not terribly user friendly, in my humble opinion. Later, I'll present a technique to retrieve really good display names from directory entries.
- ldap_explode_dn() breaks a DN into its "Relative Distinguished Name" (RDN) constituents. Optionally, the type labels can be omitted. The first value can be regarded as some kind of internal node ID. As we'll see below, this ID is usually not identical to the node's so-called display name.
- ldap_first_attribute() and ldap_next_attribute() enumerate the attributes attached to a DIT entry, similar to enumerating the entries themselves.

It's important to keep in mind that all of those functions operate on the same data block returned from the previous search request. Any data that has not been received in the LDAP reply message cannot be retrieved. For instance, if you have chosen to receive attribute names only - without their associated values - by setting the "attrsonly" flag in the ldap_search_st() call, you're not able to retrieve any attribute values in subsequent ldap_*_attribute() calls.

The ldap_*_attribute APIs return a pointer to an attribute name. The buffer that holds this name is not allocated dynamically. Instead, it's part of the LDAPMessage data received in the response to the LDAP search request. Therefore, there's no need to free this memory. Later on, when the search response has been processed, the entire LDAPMessage structure must be freed by calling ldap_msgfree().

The story is different for the other three APIs mentioned above. They return a string pointer or string array that's dynamically allocated. In the case of ldap_explode_dn(), the returned array must be freed with ldap_value_free(). This is actually a deallocation API to dispose attribute values, but since attribute values are represented by a dynamically allocated string array as well, ldap_value_free() works perfectly on exploded DNs.

ldap_get_dn() and ldap_dn2ufn() return a simple memory block containing a string. RFC 1823 (i.e. LDAPv2) recommended to use the free() function from the C run-time library (RTL). This is not a good tip for Win32 programmers, because it's perfectly OK to write Win32 programs in C without using any bit from the C RTL. The creators of LDAPv3 must have realized that, so the internet draft intended to become the replacement for RFC 1823 (draft-ietf-asid-ldap-c-api-00.txt) defines the general-purpose memory deallocator ldap_memfree() for situations like that. And even better, wldap32.dll supports this new API.

Evaluating Attributes
Attributes are the gist of the X.500 directory. While the entries reflect the structure of the DIT, attributes expose its content. In an object-oriented world, we'd refer to the entries as "objects", and to the attributes as "properties". Among other useful things, attributes provide object classes, display names, and descriptions of the directory entries.

The attribute enumeration APIs return just the names of the attributes attached to a directory entry. To retrieve the associated values, the LDAP API provides the functions ldap_get_values() and ldap_get_values_len(). That's right - the plural used in those names indicates that an attribute can have more than one value! However, most attributes are kind and don't make use of this confusing feature.

The difference between ldap_get_values() and ldap_get_values_len() is that the former returns a string array, while the latter returns an array of "berval" structures that treat the attribute values as binary quantities. A "berval" structure is simply a pointer to a sequence of bytes, preceded by an unsigned long integer indicating the length of the sequence. Example 5 below shows the definitions. I've found that it's quite OK to forget about ldap_get_values_len() when working with Exchange Server 5.x. All attributes of interest can be retrieved as strings by means of ldap_get_values().

One annoying problem with attributes is that the names and values have changed considerably from Exchange Server Version 5.0 to 5.5. Microsoft probably didn't do that to irate LDAP client programmers. Instead, they obviously tried to keep pace with the ongoing standardization of the X.500 directory attributes. However, this incompatibility makes writing a version-independent LDAP directory browser really tough.

Display Names
One of the most important directory entry attributes is the display name. If you're opening the "Exchange Server Administrator" utility admin.exe (usually found in the \exchsrvr\bin directory) and expand some branches of the DIT, the labels you see are display names. As it turns out, retrieving those names is not as trivial as it might seem. The problem is that display names are stored under different attribute names, depending on the Exchange Server version and the particular sub-tree you're currently in.

Following are the attribute names I found to yield the display name correctly under all conditions if checked in this order:

- "Admin-Display-Name": Both Exchange Server 5.0 and 5.5 use this attribute to hold the display name of all entries except those in the "Recipients" container (a.k.a. the "Global Address List").
- "Display-Name": This is the attribute that evaluates to the display name of recipients stored in the Exchange Server 5.0 directory. It's not used by version 5.5.
- "cn": Exchange 5.5 uses this property instead of the "Display-Name" for recipient display names. Under version 5.0, this property is the equivalent to the "rdn" property exposed by version 5.5 (see below). "cn" is an acronym for "Common Name".
- "rdn": This attribute is available under Exchange Server 5.5 only and returns, of course, the "Relative Distinguished Name" of an entry. This is typically the same name you would get if you extracted the first component of the entry's DN using ldap_explode_dn().

Applied to an Exchange Server 5.0 directory entry, this heuristic will usually bail out at an "Admin-Display-Name" or "Display-Name" attribute. If none of them is present, chances are good that at least  "cn" is available, which is sort of a fallback condition. In an Exchange Server 5.5 directory, either "Admin-Display-Name" or "cn" will return the desired value, and "rdn" constitutes the fallback.

Of course, you can go the easy way and always use the first component of every DN returned by the LDAP search request as the display name. However, this won't be quite satisfying, because those names tend to be very short (and hence user-unfriendly), and are not localized. In most cases, they are considerably different from what system administrators have become familiar with by using Microsoft's "Exchange Server Administrator" tool.

Server Controls
One problem your LDAP browser will meet soon is search result restriction. By default, Exchange Server 5.x is only willing to return 100 search hits at a time. This setting can be customized by running the "Exchange Server Administrator", opening the "Configuration\Protocols" entry of your Exchange site, double-clicking the "LDAP (Directory) Site defaults" entry, selecting the "Search" tab, and editing the "Maximum number of search results". However, there might be situations where you must take this value as given - for instance, if the Exchange Server you're working with is out of your reach.

With Exchange Server 5.0, this is a serious problem, because this version "speaks" LDAPv2 only. You need version 5.5 - i.e. LDAPv3 - to be able to go beyond the search result limit under client control. This is possible by means of one of the new LDAP controls introduced with version 3 of the protocol. Section 4.1.12. of RFC 2251 defines a control as "a way to specify extension information" (p. 19).

At the time of this writing, two controls are under discussion: "Server Side Sorting of Search Results" (draft-ietf-asid-ldapv3-sorting-00.txt, April 1997), and "Simple Paged Results Manipulation" (draft-ietf-asid-ldapv3-simplepaged-02.txt, February 1998). It's the latter that will help us out of the narrow walls of the search result limit.

The term "Paged Results" means that the results of a search operation are handed from the server to the client in several separate chunks called "pages". Thus, it's possible to receive an arbitrary number of entries matching a given search pattern. This is essential for a directory browser, which must be able to find all entries on any directory layer. For example, the global address list of a large corporation will easily exceed the default limit of 100. Using paged results, the "Maximum number of search results" mentioned earlier defines the maximum number of entries a page may comprise.

It's not odd coincidence that Exchange Server 5.5 supports this rather new concept, although the latest Internet Draft dates back just a few months. This type of LDAP control was designed by Chris Weider and Andy Herron, both working at Microsoft - plus Tim Howes of Netscape, who is, by the way, one of the creators of LDAPv3 / RFC 2251.

To find out if an LDAP server supports a given control, it's necessary to query the "supportedControl" attribute of the DIT's root entry (see section 5.2.4. of RFC 2252). This attribute (if present) has a list of object identifiers (OIDs) attached to it. Each OID is represented as a dotted sequence of decimal numbers and uniquely corresponds to a control supported by the server. The OID of the "Simple Paged Results" control is 1.2.840.113556.1.4.319. If the "supportedControl" attribute isn't present at all, it's highly probable that the server isn't LDAPv3 aware.

RFC 1823 and the LDAPv3 API draft (draft-ietf-asid-ldap-c-api-00.txt) don't define a dedicated API to query attributes of a given node (i.e. a "List" request according to X.500). Instead, this operation is viewed as a special case of searching. That's where the LDAP_SCOPE_BASE argument passed to ldap_search*() enters the stage. Calling ldap_search_s() or ldap_search_st() with a scope of LDAP_SCOPE_BASE and an "(objectClass=*)" filter is guaranteed to return a single LDAPMessage corresponding to the node identified by the "base" DN. This result can be used to step through the attributes of this node by calling the ldap_first_attribute() and ldap_next_attribute() APIs. If the "base" DN is an empty string, the root attributes become available.

Paged Results
Using controls the way the Internet Draft "LDAP API Extensions for Sort and Simple Paged Results" (draft-ietf-asid-ldapv3-api-ext-00.txt) proposes is quite cumbersome. Frankly, I have to admit that I didn't get the paged results control working at all. However, while examining winldap.h over and over again, I found out that Microsoft already has implemented a much easier way to get extended search result sets. To use them, your wldap32.dll file should have an internal version number of 5.0.1515.0, 5.0.1576.0 or newer. Version 4.1.26.1 found on many Windows NT 4.00 workstations is outdated and fails to export the required APIs.

Using the Microsoft APIs for paged results is extremely easy. You don't have to deal with any controls - they're well hidden from your eyes. It's just a matter of calling ldap_search_init_page() at the beginning, ldap_get_next_page_s() to collect pages of search results, and ldap_abandon_page() at the end. To find out how many entries were returned in a page, just call ldap_count_entries() on the LDAPMessage returned by ldap_get_next_page_s(), like you would in any standard LDAPv2 search request. If ldap_get_next_page_s() runs out of data, its status code is LDAP_NO_RESULTS_RETURNED.

ldap_search_init_page() returns a "handle" pointing to an LDAPSearch structrure. Although its definition can be looked up in winldap.h, LDAPSearch should be treated as being opaque, and the structure members should not be accessed directly.

Before using the paging APIs, you must insure that the server at the other end of the line supports paged results in the first place. The safest way is to search the directory's root entry for the attribute "supportedControl" and scan the available attribute values for a string of "1.2.840.113556.1.4.319". For convenience, this string is predefined in winldap.h as LDAP_PAGED_RESULT_OID_STRING (ANSI) and LDAP_PAGED_RESULT_OID_STRING_W (Unicode). If the attribute itself isn't present, you should assume that the server doesn't support any controls.

Version Negotiation
Another possibility to test for support of version-specific functionality is to query the LDAP version supported by the server. Again, this information can be gained by retrieving DIT root attribute values. In this case, the attribute name is "supportedVersion". Note that this attribute is not present under Exchange Server 5.0. Generally, you should assume LDAPv2 (defined as LDAP_VERSION2 in winldap.h) and absence of all server controls if the "supportedVersion" attribute is missing. Exchange Server 5.5 returns the values "3" and "2", which means that both LDAPv3 and LDAPv2 APIs can be called.

Since the LDAP APIs are implemented on the client side, nobody can prevent you from trying to use result paging and other LDAPv3 extensions with Exchange Server 5.0. Interestingly, those APIs work somehow, but not reliably. For instance, I've run into problems with local characters in DNs (missing characters, invalid trailing characters, etc.). So the basic rule is: Always match the LDAP versions on the client and server sides.

After finding out which LDAP versions the server supports, it's necessary to set up the client to select among them. By default, an LDAP client is supposed to use LDAPv2. To enable the client for LDAPv3, you must call ldap_set_option() with an option ID of LDAP_OPT_PROTOCOL_VERSION. Although this API isn't present under LDAPv2 (RFC 1823), you can still call it even if your server is version 5.0 only. In fact, my empirical tests have shown that you SHOULD do so for optimal results.

The optimal way of version negotiation found by excessive testing is the following:

- Open a connection using ldap_init().
- Query the "supportedVersion" root attribute. If not available, assume LDAP_VERSION2. Otherwise, loop through the attribute values and find the maximum among them.
- Call ldap_set_option(), passing in the connection handle, LDAP_OPT_PROTOCOL_VERSION, and the LDAP version number determined before.

Only after those steps are done, you are ready to call ldap_connect() and ldap_bind_s() and to perform operations on the directory. Moreover, you should never change the client's LDAP version on an active connection, or you might get unexpected results.

The LDAP Library
Although the LDAP API works on a high abstraction level and saves you from many scary implementation details of the LDAP protocol, some gaps still remain. To fill them, I've written a general-purpose LDAP library named LdapLib.dll. It hides all those nasty LDAP details from you, like version negotiation and server authentication, and transparently handles paged search results if appropriate.

The complete set of LdapLib project files, plus those of a console-oriented LDAP DIT dump utility (LdapDump.exe) demonstrating the usage of many of the APIs exported by LdapLib.dll, are available electronically. Both projects can be built with Microsoft Visual C/C++ V5.0. No linker options need to be set manually - they are included as #pragma directives in the header files.

At the core of the browsing functions in LdapLib.c, there's LdapFindCollect(), shown in Listing 2. It tests for LDAPv3 and, if available, uses Microsoft's paged results APIs. Otherwise, it falls back to the LDAPv2 standard API ldap_search_st(). LdapFindCollect() dynamically allocates and fills a linked list of LDAP_FIND structures (definition shown in Listing 1), where each list item corresponds to a result page. In the special case of LDAPv2, only a single list item is created.

This linked list approach enables other LdapLib.dll APIs, like LdapFindNext() and LdapFindCount(), to transparently handle search results with or without paging, by simply walking the LDAP_FIND list and evaluating all entries available from each list item. In this context, an LDAPv2 search result is equivalent to an LDAPv3 result that contains just a single page. To clean up the linked list, just call LdapFindCleanup() passing in its anchor (i.e the pointer to the first LDAP_FIND structure).

Since the source code of LdapLib.c consists of 1037 lines, it's impossible to reprint it here in its entirety. Listing 3 demonstrates another essential excerpt that handles LDAP initialization and binding (i.e. server authentication). LdapConnect() receives and initializes an LDAP_CONN structure, defined in Listing 1.

The arguments ptServer, ptAccount, and ptPassword are used in the authentication step (see the ldap_bind_s() call in Listing 3). ptServer is the DNS name or IP address of the LDAP server. ptAccount specifies the ID of the user that should be logged in, and ptPassword is an optional password. If the latter is not NULL, "simple authentication" (LDAP_AUTH_SIMPLE) is used. Otherwise, the server is directed to use NT LAN Manager authentication (LDAP_AUTH_NTLM). ptAccount is either a single NT account name, or a domain\name pair. LdapConnect() calls the LdapLib.dll API LdapNameFromAccount() to convert the specified string to the DN format required by the LDAP protocol ("cn=<name>" or "dc=<domain>,cn=<name>").

The remaining arguments - dPort, dVersion, dPage, and dTimeout - are connection options that may affect the behavior of subsequent LDAP requests. The easiest way to set them correctly is to pass in the corresponding LDAP_PRESET_* constants defined in LdapLib.h. This will select reasonable default values. dPort is the TCP port used to connect to the server, and its default value is 389. dVersion specifies the required LDAP version, with a default value of 2, corresponding to LDAPv2.

dPage indicates the maximum page size to be used in LDAPv3 search requests. To avoid a return code of LDAP_ADMIN_LIMIT_EXCEEDED, this value should always be selected less or equal to the value entered for the "Maximum number of search results" option by the Exchange admin. It defaults to 100, which is the preset used by the Exchange Server setup and hence should be OK in most real-life scenarios. If dPage is set to 0, paged results are disabled, and the LdapFindCollect() API refrains from using paged results, even if it detects a server version of LDAPv3 or higher.

The dTimeout argument indicates the number of milliseconds the client is willing to wait on remote operations. It is used on the LDAP API calls ldap_connect(), ldap_search_st(), and ldap_get_next_page_s(). Its default value is 10000. This is just enough to avoid timing out on lengthy search operations on slow servers. Note that the LDAP API requires an LDAP_TIMEVAL structure to specify timeout values. LdapLib.dll introduces scalar millisecond values instead (which are much easier to handle, I think), and converts them transparently before all LDAP calls that involve a timeout.

There are dozens of other interesting things that could be said about the internals of LdapLib.dll. For instance, how it saves you from writing recursive code to work through the levels of the server directory. Alas, there's not enough space left here, so I have to direct you to the source code of the LdapDump.exe utility available online (see p. 3, "Resource Center", for options). Search LdapDump.c for the function DisplayDirectory() and see how it uses LdapConnect(), LdapEntryRecurse(), and LdapDisconnect() to do most of the work. LdapEntryRecurse() invokes a callback function on every entry matching the search request, and provides extensive information on the location of the entry in the DIT.

Example 6 shows an excerpt of a console output generated by LdapDump.exe scanning an Exchange Server 5.5 directory with attributes enabled (command line option /a).


Example 1: ASN.1-style definition of an LDAP "BindRequest" packet.

BindRequest ::= [APPLICATION 0] SEQUENCE {
        version                 INTEGER (1 .. 127),
        name                    LDAPDN,
        authentication          AuthenticationChoice }

Example 2: The CHOICE data type selects among alternative types.

AuthenticationChoice ::= CHOICE {
        simple                  [0] OCTET STRING,
                                 -- 1 and 2 reserved
        sasl                    [3] SaslCredentials }

Example 3: ldap_search_st() is the heavy-duty API used by a directory browser.

WINLDAPAPI ULONG LDAPAPI
ldap_search_st (LDAP              *ld,
                PCHAR              base,
                ULONG              scope,
                PCHAR              filter,
                PCHAR              attrs [],
                ULONG              attrsonly,
                struct l_timeval  *timeout,
                LDAPMessage      **res);

Example 4: ldap_first_entry() and ldap_next_entry() navigate through a search response.

WINLDAPAPI LDAPMessage *LDAPAPI
ldap_first_entry (LDAP        *ld,
                  LDAPMessage *res);

WINLDAPAPI LDAPMessage *LDAPAPI
ldap_next_entry (LDAP        *ld,
                 LDAPMessage *entry);

Example 5: ldap_get_values_len() and ldap_get_values() return the values of an attribute.

typedef struct berval
    {
    ULONG  bv_len;
    PCHAR  bv_val;
    }
    LDAP_BERVAL, *PLDAP_BERVAL;

WINLDAPAPI struct berval **LDAPAPI
ldap_get_values_len (LDAP        *ld,
                     LDAPMessage *entry,
                     PCHAR        attr);

WINLDAPAPI PCHAR *LDAPAPI
ldap_get_values (LDAP        *ld,
                 LDAPMessage *entry,
                 PCHAR        attr);

Listing 1: LdapLib uses a linked list of LDAP_FIND structures to keep track of search results.

typedef struct _LDAP_CONN
    {
    PLDAP        pl;
    DWORD        dPort;
    DWORD        dVersion;
    DWORD        dPage;
    DWORD        dTimeout;
    LDAP_TIMEVAL ltTimeout;
    TBYTE atUser [N_DN];
    }
    LDAP_CONN, *PLDAP_CONN, **PPLDAP_CONN;
#define LDAP_CONN_ sizeof (LDAP_CONN)

typedef struct _LDAP_FIND
    {
    struct _LDAP_FIND *plf;
    struct _LDAP_FIND *plfNext;
    PLDAP_CONN         plc;
    PLDAPMessage       plm;
    PLDAPMessage       plmNext;
    DWORD              dEntries;
    }
    LDAP_FIND, *PLDAP_FIND, **PPLDAP_FIND;
#define LDAP_FIND_ sizeof (LDAP_FIND)

Listing 2: LdapFindCollect() enumerates all entries available at the specified directory node.

DWORD EXPORT LdapFindCollect (PPLDAP_FIND pplf,
                              PLDAP_CONN  plc,
                              PTBYTE      ptDn,
                              PTBYTE      ptFilter,
                              PPTBYTE     pptAttr,
                              DWORD       dScope)
    {
    PLDAPSearch pls;
    PLDAP_FIND  plf, plfNext;
    DWORD       dCount;
    DWORD       dStatus = LDAP_OTHER;

    plf = NULL;

    if (LdapMemoryCreate (&plf, LDAP_FIND_))
        {
        plf->plf         = plf;
        plf->plfNext     = NULL;
        plf->plc         = plc;
        plf->plm         = NULL;
        plf->plmNext     = NULL;
        plf->dEntries    = 0;

        if (plc->dPage && (plc->dVersion >= 3))
            {
            pls = ldap_search_init_page (plc->pl, ptDn, dScope,
                                         ptFilter, pptAttr,
                                         FALSE, NULL, NULL,
                                         0, 0, NULL);
            if (pls != NULL)
                {
                plfNext = plf;

                while ((dStatus = ldap_get_next_page_s
                                      (plc->pl, pls,
                                       &plc->ltTimeout, plc->dPage,
                                       &dCount, &plfNext->plm))
                       == LDAP_SUCCESS)
                    {
                    plfNext->dEntries = ldap_count_entries
                                            (plc->pl, plfNext->plm);

                    if (LdapMemoryCreate (&plfNext->plfNext,
                                          LDAP_FIND_))
                        {
                        plfNext = plfNext->plfNext;
                        plfNext->plf      = NULL;
                        plfNext->plfNext  = NULL;
                        plfNext->plc      = plc;
                        plfNext->plm      = NULL;
                        plfNext->plmNext  = NULL;
                        plfNext->dEntries = 0;
                        }
                    else
                        {
                        dStatus = LDAP_NO_MEMORY;
                        break;
                        }
                    }
                ldap_search_abandon_page (plc->pl, pls);
                }
            else
                {
                dStatus = LdapError ();
                }
            }
        else
            {
            if ((dStatus = ldap_search_st (plc->pl, ptDn, dScope,
                                           ptFilter, pptAttr, FALSE,
                                           &plc->ltTimeout,
                                           &plf->plm))
                == LDAP_SUCCESS)
                {
                plf->dEntries = ldap_count_entries (plc->pl,
                                                    plf->plm);
                }
            }
        }
    else
        {
        dStatus = LDAP_NO_MEMORY;
        }
    *pplf = plf;
    if (dStatus == LDAP_NO_RESULTS_RETURNED) dStatus = LDAP_SUCCESS;
    return dStatus;
    }

Listing 3: Connecting to an LDAP server involves several steps.

DWORD EXPORT LdapConnect (PLDAP_CONN plc,
                          PTBYTE     ptServer,
                          PTBYTE     ptAccount,
                          PTBYTE     ptPassword,
                          DWORD      dPort,
                          DWORD      dVersion,
                          DWORD      dPage,
                          DWORD      dTimeout)
    {
    DWORD dStatus = LDAP_OTHER;
    plc->pl = NULL;

    plc->dPort    = (dPort    != LDAP_PRESET_PORT    ?  dPort
                                                     : gdPort);

    plc->dVersion = (dVersion != LDAP_PRESET_VERSION ?  dVersion
                                                     : gdVersion);

    plc->dPage    = (dPage    != LDAP_PRESET_PAGE    ?  dPage
                                                     : gdPage);

    plc->dTimeout = (dTimeout != LDAP_PRESET_TIMEOUT ?  dTimeout
                                                     : gdTimeout);

    plc->ltTimeout.tv_sec  = (plc->dTimeout / 1000);
    plc->ltTimeout.tv_usec = (plc->dTimeout % 1000) * 1000;

    LdapNameFromAccount (ptAccount,
                         plc->atUser, sizeof (plc->atUser));

    if ((plc->pl = ldap_init ((*ptServer ? ptServer : NULL),
                              plc->dPort))
        != NULL)
        {
        LdapVersionSet (plc, plc->dVersion);

        if (((dStatus = ldap_connect (plc->pl, &plc->ltTimeout))
             != LDAP_SUCCESS)
            ||
            ((dStatus = ldap_bind_s (plc->pl,
                                     plc->atUser, ptPassword,
                                     (ptPassword != NULL
                                      ? LDAP_AUTH_SIMPLE
                                      : LDAP_AUTH_NTLM)))
             != LDAP_SUCCESS))
            {
            LdapDisconnect (plc);
            }
        }
    else
        {
        dStatus = LdapError ();
        }
    return dStatus;
    }

14


