1 ///
2 module ldap;
3 
4 import std.algorithm;
5 import std.array;
6 import std.conv;
7 import std.string;
8 import std.utf;
9 
10 ///
11 class LDAPException : Exception
12 {
13 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe
14 	{
15 		super(msg, file, line);
16 	}
17 }
18 
19 /// If username has a domain specified, this will split it into a username and domain and return them in a string array.
20 /// Params:
21 ///   username: username to split if domain is specified
22 ///   user: out parameter that will receive the username
23 ///   domain: out parameter that will receive the domain. Empty if domain is not part of username
24 void domainSplit(string username, out string user, out string domain)
25 {
26 	import std.algorithm.searching : findSplit;
27 
28 	if (auto ret = username.findSplit("@"))
29 	{
30 		user = ret[0];
31 		domain = ret[2];
32 	}
33 	else if (auto ret = username.findSplit("\\"))
34 	{
35 		user = ret[2];
36 		domain = ret[0];
37 	}
38 	else
39 		user = username;
40 }
41 
42 version (Windows)
43 {
44 	public import core.sys.windows.winldap;
45 	public import core.sys.windows.winber;
46 
47 	//dfmt off
48 	private enum LDAPErrorCodes
49 	{ // Taken from core.sys.windows.winldap
50 		LDAP_SUCCESS = 0x00,
51 		LDAP_OPT_SUCCESS = LDAP_SUCCESS,
52 		LDAP_OPERATIONS_ERROR,
53 		LDAP_PROTOCOL_ERROR,
54 		LDAP_TIMELIMIT_EXCEEDED,
55 		LDAP_SIZELIMIT_EXCEEDED,
56 		LDAP_COMPARE_FALSE,
57 		LDAP_COMPARE_TRUE,
58 		LDAP_STRONG_AUTH_NOT_SUPPORTED,
59 		LDAP_AUTH_METHOD_NOT_SUPPORTED = LDAP_STRONG_AUTH_NOT_SUPPORTED,
60 		LDAP_STRONG_AUTH_REQUIRED,
61 		LDAP_REFERRAL_V2,
62 		LDAP_PARTIAL_RESULTS = LDAP_REFERRAL_V2,
63 		LDAP_REFERRAL,
64 		LDAP_ADMIN_LIMIT_EXCEEDED,
65 		LDAP_UNAVAILABLE_CRIT_EXTENSION,
66 		LDAP_CONFIDENTIALITY_REQUIRED,
67 		LDAP_SASL_BIND_IN_PROGRESS, // = 0x0e
68 		LDAP_NO_SUCH_ATTRIBUTE = 0x10,
69 		LDAP_UNDEFINED_TYPE,
70 		LDAP_INAPPROPRIATE_MATCHING,
71 		LDAP_CONSTRAINT_VIOLATION,
72 		LDAP_TYPE_OR_VALUE_EXISTS,
73 		LDAP_ATTRIBUTE_OR_VALUE_EXISTS = LDAP_TYPE_OR_VALUE_EXISTS,
74 		LDAP_INVALID_SYNTAX, // = 0x15
75 		LDAP_NO_SUCH_OBJECT = 0x20,
76 		LDAP_ALIAS_PROBLEM,
77 		LDAP_INVALID_DN_SYNTAX,
78 		LDAP_IS_LEAF,
79 		LDAP_ALIAS_DEREF_PROBLEM, // = 0x24
80 		LDAP_INAPPROPRIATE_AUTH = 0x30,
81 		LDAP_INVALID_CREDENTIALS,
82 		LDAP_INSUFFICIENT_ACCESS,
83 		LDAP_INSUFFICIENT_RIGHTS = LDAP_INSUFFICIENT_ACCESS,
84 		LDAP_BUSY,
85 		LDAP_UNAVAILABLE,
86 		LDAP_UNWILLING_TO_PERFORM,
87 		LDAP_LOOP_DETECT, // = 0x36
88 		LDAP_NAMING_VIOLATION = 0x40,
89 		LDAP_OBJECT_CLASS_VIOLATION,
90 		LDAP_NOT_ALLOWED_ON_NONLEAF,
91 		LDAP_NOT_ALLOWED_ON_RDN,
92 		LDAP_ALREADY_EXISTS,
93 		LDAP_NO_OBJECT_CLASS_MODS,
94 		LDAP_RESULTS_TOO_LARGE,
95 		LDAP_AFFECTS_MULTIPLE_DSAS, // = 0x47
96 		LDAP_OTHER = 0x50,
97 		LDAP_SERVER_DOWN,
98 		LDAP_LOCAL_ERROR,
99 		LDAP_ENCODING_ERROR,
100 		LDAP_DECODING_ERROR,
101 		LDAP_TIMEOUT,
102 		LDAP_AUTH_UNKNOWN,
103 		LDAP_FILTER_ERROR,
104 		LDAP_USER_CANCELLED,
105 		LDAP_PARAM_ERROR,
106 		LDAP_NO_MEMORY,
107 		LDAP_CONNECT_ERROR,
108 		LDAP_NOT_SUPPORTED,
109 		LDAP_CONTROL_NOT_FOUND,
110 		LDAP_NO_RESULTS_RETURNED,
111 		LDAP_MORE_RESULTS_TO_RETURN,
112 		LDAP_CLIENT_LOOP,
113 		LDAP_REFERRAL_LIMIT_EXCEEDED // = 0x61
114 	}
115 	//dfmt on
116 
117 	string ldapWinErrorToString(uint err) pure nothrow @safe
118 	{
119 		try
120 		{
121 			return (cast(LDAPErrorCodes) err).to!string;
122 		}
123 		catch (Exception)
124 		{
125 			return err.to!string;
126 		}
127 	}
128 
129 	/// Exception thrown if a connection cannot be established to the LDAP server.
130 	class LDAPConnectionException : LDAPException
131 	{
132 		this(string host, uint errCode, string file = __FILE__, size_t line = __LINE__)
133 		{
134 			super("Failed to connect to " ~ host ~ ": " ~ errCode.ldap_err2string.to!string
135 					~ " (Error code " ~ errCode.to!string ~ ", " ~ errCode.ldapWinErrorToString ~ ")",
136 					file, line);
137 		}
138 	}
139 
140 	pragma(inline, true) auto enforceLDAP(string fn, string file = __FILE__, size_t line = __LINE__)(
141 			lazy uint f)
142 	{
143 		auto ret = f();
144 		if (ret != LDAP_SUCCESS)
145 			throw new LDAPException("LDAP Error '" ~ ret.ldap_err2string.to!string ~ "' in " ~ fn
146 					~ " (Error code " ~ ret.to!string ~ ", " ~ ret.ldapWinErrorToString ~ ")", file, line);
147 	}
148 
149 	enum LDAP_OPT_FAST_CONCURRENT_BIND = 0x41;
150 
151 	extern (C) uint ldap_search_ext_sW(LDAP*, wchar*, uint, wchar*, wchar**,
152 			uint, PLDAPControlW*, PLDAPControlW*, LDAP_TIMEVAL*, uint, LDAPMessage**);
153 
154 	alias mPLDAPControl = PLDAPControlW;
155 
156 	PLDAP ldapInit(string host)
157 	{
158 		return ldap_initW(cast(wchar*) host.toUTF16z, 389);
159 	}
160 
161 	uint ldapBind(PLDAP _handle, string user, string cred, int method)
162 	{
163 		if (method == LDAP_AUTH_SIMPLE)
164 			return ldap_bind_sW(_handle, cast(wchar*) user.toUTF16z, cast(wchar*) cred.toUTF16z, method);
165 		else // implies NTLM for now
166 		{
167 			// for encrypting the credentials, domain must be specified in the username (DOMAIN\username or username@DOMAIN) if this is used
168 			import core.sys.windows.rpcdce;
169 			string _user, _domain;
170 
171 			user.domainSplit(_user, _domain);
172 
173 			SEC_WINNT_AUTH_IDENTITY id =
174 			SEC_WINNT_AUTH_IDENTITY(
175 				cast(ushort*) _user.toUTF16z, // User
176 				cast(uint) _user.length, //UserLength
177 				cast(ushort*) _domain.toUTF16z, //Domain
178 				cast(uint) _domain.length, //DomainLength
179 				cast(ushort*) cred.toUTF16z, //Password
180 				cast(uint) cred.length, //PasswordLength
181 				SEC_WINNT_AUTH_IDENTITY_UNICODE // flags
182 			);
183 
184 			return ldap_bind_sW(_handle, null, cast(wchar*) &id, method);
185 		}
186 	}
187 }
188 else
189 {
190 	import core.sys.posix.sys.time;
191 
192 	struct berval
193 	{
194 		int bv_len;
195 		char* bv_val;
196 	}
197 
198 	alias BerValue = berval;
199 	struct ldapcontrol
200 	{
201 		char* ldctl_oid; /* numericoid of control */
202 		berval ldctl_value; /* encoded value of control */
203 		char ldctl_iscritical; /* criticality */
204 	};
205 	alias LDAPControl = ldapcontrol;
206 	alias mPLDAPControl = LDAPControl*;
207 
208 	alias PLDAPMessage = void*;
209 	alias PLDAP = void*;
210 
211 	struct SEC_WINNT_AUTH_IDENTITY
212 	{
213 		ubyte* User;
214 		uint UserLength;
215 		ubyte* Domain;
216 		uint DomainLength;
217 		ubyte* Password;
218 		uint PasswordLength;
219 		uint Flags;
220 	};
221 
222 	extern (C) void ldap_msgfree(void*);
223 	extern (C) void ldap_memfree(void*);
224 	extern (C) void ber_free(void*, int);
225 	extern (C) void ldap_value_free(char**);
226 
227 	extern (C) int ldap_get_option(void* ld, int option, void* outvalue);
228 	extern (C) int ldap_set_option(void* ld, int option, const void* invalue);
229 
230 	extern (C) int ldap_initialize(void**, const char*);
231 	extern (C) char* ldap_err2string(int);
232 	extern (C) void* ldap_first_entry(void* ld, void* chain);
233 	extern (C) void* ldap_next_entry(void* ld, void* entry);
234 	extern (C) char* ldap_get_dn(void* ld, void* entry);
235 	extern (C) int ldap_search_ext_s(void* ld, const char* base, int _scope, const char* filter,
236 			char** attrs, int attrsonly, LDAPControl** serverControls,
237 			LDAPControl** clientControls, timeval* timeout, int sizeLimit, void** res); // LDAPMessage
238 	extern (C) char* ldap_first_attribute(void* ld, void* entry, void** ber);
239 	extern (C) char* ldap_next_attribute(void* ld, void* entry, void* ber);
240 	extern (C) char** ldap_get_values(void* ld, void* entry, char* attr);
241 	extern (C) int ldap_count_entries(PLDAP, void*);
242 	extern (C) int ldap_count_values(char**);
243 
244 	extern (C) int ldap_bind_s(void* ld, const char* who, const char* cred, int method);
245 	extern (C) int ldap_unbind(void* ld);
246 	alias ldap_unbind_s = ldap_unbind;
247 
248 	enum LDAP_SCOPE_BASE = 0x0000, LDAP_SCOPE_BASEOBJECT = LDAP_SCOPE_BASE,
249 			LDAP_SCOPE_ONELEVEL = 0x0001, LDAP_SCOPE_ONE = LDAP_SCOPE_ONELEVEL, LDAP_SCOPE_SUBTREE = 0x0002,
250 			LDAP_SCOPE_SUB = LDAP_SCOPE_SUBTREE, LDAP_SCOPE_SUBORDINATE = 0x0003, /* OpenLDAP extension */
251 			LDAP_SCOPE_CHILDREN = LDAP_SCOPE_SUBORDINATE, LDAP_SCOPE_DEFAULT = -1; /* OpenLDAP extension */
252 
253 	enum LDAP_SUCCESS = 0;
254 	enum LDAP_AUTH_SIMPLE = 0x80U;
255 	enum LDAP_AUTH_NTLM = 0x1086U;
256 	enum void* LDAP_OPT_OFF = null, LDAP_OPT_ON = cast(void*) 1;
257 
258 	enum LDAP_OPT_PROTOCOL_VERSION = 0x0011U;
259 	enum LDAP_OPT_FAST_CONCURRENT_BIND = 0x41;
260 	alias PLDAP_TIMEVAL = timeval*;
261 
262 	enum SEC_WINNT_AUTH_IDENTITY_ANSI=0x1;
263 
264 	class LDAPConnectionException : LDAPException
265 	{
266 		this(string host, uint errCode, string file = __FILE__, size_t line = __LINE__)
267 		{
268 			super("Failed to connect to " ~ host ~ ": " ~ errCode.ldap_err2string.to!string
269 					~ " (Error code " ~ errCode.to!string ~ ")", file, line);
270 		}
271 	}
272 
273 	pragma(inline, true) auto enforceLDAP(string fn, string file = __FILE__, size_t line = __LINE__)(
274 			lazy uint f)
275 	{
276 		auto ret = f();
277 		if (ret != LDAP_SUCCESS)
278 			throw new LDAPException("LDAP Error '" ~ ret.ldap_err2string.to!string
279 					~ "' in " ~ fn ~ " (Error code " ~ ret.to!string ~ ")", file, line);
280 	}
281 
282 	PLDAP ldapInit(string host)
283 	{
284 		void* ret;
285 		enforceLDAP!"initialize"(ldap_initialize(&ret, host.toStringz));
286 		return ret;
287 	}
288 
289 	enum LdapGetLastError = 0;
290 
291 	uint ldapBind(PLDAP _handle, string user, string cred, int method)
292 	{
293 		if (method == LDAP_AUTH_SIMPLE)
294 			return ldap_bind_s(_handle, user.toStringz, cred.toStringz, method);
295 		else
296 		{
297 			// for encrypting the credentials
298 			string _user, _domain;
299 
300 			user.domainSplit(_user, _domain);
301 
302 			SEC_WINNT_AUTH_IDENTITY id =
303 			SEC_WINNT_AUTH_IDENTITY(
304 				cast(ubyte*) _user.toStringz, // User
305 				cast(uint) _user.length, //UserLength
306 				cast(ubyte*) _domain.toStringz, //Domain
307 				cast(uint) _domain.length, //DomainLength
308 				cast(ubyte*) cred.toStringz, //Password
309 				cast(uint) cred.length, //PasswordLength
310 				SEC_WINNT_AUTH_IDENTITY_ANSI // flags
311 			);
312 
313 			return ldap_bind_s(_handle, null, cast(char*) &id, method);
314 		}
315 	}
316 }
317 
318 enum LDAPSearchScope : uint
319 {
320 	base_ = LDAP_SCOPE_BASE,
321 	oneLevel = LDAP_SCOPE_ONELEVEL,
322 	subTree = LDAP_SCOPE_SUBTREE
323 }
324 
325 /// LDAP class to do any kind of search in the directory.
326 struct LDAPConnection
327 {
328 	/// Pointer to internal (platform-dependent) connection handle. Use with care.
329 	PLDAP _handle;
330 
331 	/// Connects to the LDAP server using the given host.
332 	/// Params:
333 	///     host: Host name and port separated with a colon (:)
334 	this(string host)
335 	{
336 		version (Windows)
337 		{
338 		}
339 		else
340 			host = host.split(' ').map!(a => "ldap://" ~ a).join(' ');
341 		_handle = ldapInit(host); // The host can contain a port separated with a colon (:) to override this default port
342 		if (_handle is null)
343 			throw new LDAPConnectionException(host, LdapGetLastError);
344 		version (Windows)
345 			enforceLDAP!"connect"(ldap_connect(_handle, null));
346 		else
347 			bind("", "");
348 	}
349 
350 	/// Connects to the LDAP server by trying every host until it can find one.
351 	/// Params:
352 	///     hosts: List of host names and ports separated with a colon (:)
353 	this(string[] hosts)
354 	{
355 		this(hosts.join(' '));
356 	}
357 
358 	~this()
359 	{
360 		unbind();
361 	}
362 
363 	/// Terminates the connection.
364 	void unbind()
365 	{
366 		ldap_unbind_s(_handle);
367 	}
368 
369 	/// Sets an option to an arbitrary value (See https://msdn.microsoft.com/en-us/library/aa366993(v=vs.85).aspx)
370 	void setOption(int option, void* value)
371 	{
372 		enforceLDAP!"setOption"(ldap_set_option(_handle, option, value));
373 	}
374 
375 	/// Returns the current value of an option.
376 	void getOption(int option, void* value)
377 	{
378 		enforceLDAP!"getOption"(ldap_get_option(_handle, option, value));
379 	}
380 
381 	/// Synchronously authenticates a client to the LDAP server.
382 	/// Throws: a LDAPException on failure.
383 	void bind(string user, string cred, int method = LDAP_AUTH_SIMPLE)
384 	{
385 		enforceLDAP!"bind"(ldapBind(_handle, user, cred, method));
386 	}
387 
388 	/// Synchronously search the LDAP directory and return entries with attributes.
389 	/// Params:
390 	///   searchBase = String that contains the distinguished name of the entry at which to start the search. (For Example OU=data,DC=data,DC=local)
391 	///   searchScope = Scope to search in (base, oneLevel, subTree)
392 	///   filters = Filters to apply on the results. See https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx
393 	///   attrs = Array to strings which attributes to return. Pass null for all.
394 	///   attrsonly = Boolean that should be false if both attribute types and values are to be returned. true for only types.
395 	///   serverControls = A list of LDAP server controls.
396 	///   clientControls = A list of client controls.
397 	///   timeout = Combined search and server operation timelimit.
398 	///   sizeLimit = limit of the number of entries to return. 0 for unlimited.
399 	/// Returns: Entries with the requested attributes.
400 	SearchResult[] search(string searchBase, LDAPSearchScope searchScope,
401 			string filter = "(objectClass=*)", string[] attrs = null,
402 			bool attrsonly = false, mPLDAPControl serverControls = null,
403 			mPLDAPControl clientControls = null, PLDAP_TIMEVAL timeout = null, int sizeLimit = 0)
404 	{
405 		PLDAPMessage res;
406 		scope (failure)
407 			if (res)
408 				ldap_msgfree(res);
409 		version (Windows)
410 			enforceLDAP!"search"(ldap_search_ext_sW(_handle, cast(wchar*) searchBase.toUTF16z,
411 					cast(uint) searchScope, cast(wchar*) filter.toUTF16z, attrs is null
412 					? null : (attrs.map!(a => cast(wchar*) a.toUTF16z).array ~ null).ptr,
413 					attrsonly ? 1 : 0, &serverControls, &clientControls, timeout, sizeLimit, &res));
414 		else
415 			enforceLDAP!"search"(ldap_search_ext_s(_handle, cast(char*) searchBase.toStringz,
416 					cast(uint) searchScope, cast(char*) filter.toStringz, attrs is null
417 					? null : (attrs.map!(a => cast(char*) a.toStringz).array ~ null).ptr,
418 					attrsonly ? 1 : 0, &serverControls, &clientControls, timeout, sizeLimit, &res));
419 		SearchResult[] results;
420 		results.length = cast(size_t) ldap_count_entries(_handle, res);
421 		PLDAPMessage entry;
422 
423 		// Would have used ranges, but memory gets corrupted
424 		foreach (i, ref result; results)
425 		{
426 			if (i == 0)
427 				entry = ldap_first_entry(_handle, res);
428 			else
429 				entry = ldap_next_entry(_handle, entry);
430 			if (entry is null)
431 				throw new LDAPException("Failed to read entry");
432 
433 			version (Windows)
434 			{
435 				wchar* dn = ldap_get_dnW(_handle, entry);
436 				if (dn is null)
437 					throw new LDAPException("Failed to read entry information");
438 				result.distinguishedName = dn.to!string.idup;
439 				ldap_memfreeW(dn);
440 			}
441 			else
442 			{
443 				char* dn = ldap_get_dn(_handle, entry);
444 				if (dn is null)
445 					throw new LDAPException("Failed to read entry information");
446 				result.distinguishedName = dn.to!string.idup;
447 				ldap_memfree(dn);
448 			}
449 
450 			version (Windows)
451 				BerElement* berP;
452 			else
453 				void* berP;
454 			for (auto attr = ldap_first_attribute(_handle, entry, &berP); attr !is null;
455 					attr = ldap_next_attribute(_handle, entry, berP))
456 			{
457 				string attr_name = attr.to!string;
458 
459 				auto ppValue = ldap_get_values(_handle, entry, attr);
460 				if (!ppValue)
461 				{
462 					result.attributes[attr_name] = [];
463 				}
464 				else
465 				{
466 					auto iValue = ldap_count_values(ppValue);
467 					if (!iValue)
468 						result.attributes[attr_name] = [];
469 					else
470 						result.attributes[attr_name] = ppValue[0 .. iValue].map!(a => a.to!(char[])
471 								.idup).array;
472 					ldap_value_free(ppValue);
473 					ppValue = null;
474 				}
475 
476 				ldap_memfree(attr);
477 			}
478 			ber_free(berP, 0);
479 		}
480 		return results;
481 	}
482 }
483 
484 ///
485 struct SearchResult
486 {
487 	/// The path to the entry like `CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM`. See https://msdn.microsoft.com/en-us/library/aa366101(v=vs.85).aspx
488 	string distinguishedName;
489 	/// Attributes of this object
490 	string[][string] attributes;
491 }
492 
493 /// Struct to check if credentials are able to authenticate on the LDAP server.
494 struct LDAPAuthenticationEngine
495 {
496 	/// Pointer to internal (platform-dependent) connection handle. Use with care.
497 	PLDAP _handle;
498 	private bool encrypted;
499 
500 	/// Connects to the LDAP server using the given host.
501 	/// Params:
502 	///     host: Host name and port separated with a colon (:)
503 	///     encrypted: If true, credentials are encrypted when sent to be authenticated
504 	this(string host, bool encrypted)
505 	{
506 		this.encrypted = encrypted;
507 		version (Windows)
508 		{
509 		}
510 		else
511 			host = host.split(' ').map!(a => "ldap://" ~ a).join(' ');
512 		_handle = ldapInit(host); // The host can contain a port separated with a colon (:) to override this default port
513 		if (_handle is null)
514 			throw new LDAPConnectionException(host, LdapGetLastError);
515 		version (Windows)
516 		{
517 			enforceLDAP!"connect"(ldap_connect(_handle, null));
518 			setOption(encrypted ? LDAP_OPT_ENCRYPT : LDAP_OPT_FAST_CONCURRENT_BIND, LDAP_OPT_ON);
519 		}
520 	}
521 
522 	/// Connects to the LDAP server by trying every host until it can find one.
523 	/// Params:
524 	///     hosts: List of host names and ports separated with a colon (:)
525 	///     encrypted: If true, credentials are encrypted when sent to be authenticated
526 	this(string[] hosts, bool encrypted)
527 	{
528 		this(hosts.join(' '), encrypted);
529 	}
530 
531 	~this()
532 	{
533 		unbind();
534 	}
535 
536 	/// Terminates the connection.
537 	void unbind()
538 	{
539 		ldap_unbind_s(_handle);
540 	}
541 
542 	/// Sets an option to an arbitrary value (See https://msdn.microsoft.com/en-us/library/aa366993(v=vs.85).aspx)
543 	void setOption(int option, void* value)
544 	{
545 		enforceLDAP!"setOption"(ldap_set_option(_handle, option, value));
546 	}
547 
548 	/// Returns the current value of an option.
549 	void getOption(int option, void* value)
550 	{
551 		enforceLDAP!"getOption"(ldap_get_option(_handle, option, value));
552 	}
553 
554 	/// Checks if a user can login with the credentials. (username should be in format `username`, `username@DOMAIN`or `DOMAIN\username`). Attempts to bind with the credentials using simple auth if encrypted was specified as false, else will use NTLM. Username must contain DOMAIN if encrypted was specified as true. Returns true if it was successful.
555 	bool check(string user, string cred)
556 	{
557 		return ldapBind(_handle, user, cred, encrypted ? LDAP_AUTH_NTLM : LDAP_AUTH_SIMPLE) == LDAP_SUCCESS;
558 	}
559 }