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 }