namespace Unosquare.Swan.Networking.Ldap { using System; using System.Collections.Generic; using System.Linq; using System.IO; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Exceptions; /// /// The central class that encapsulates the connection /// to a directory server through the Ldap protocol. /// LdapConnection objects are used to perform common Ldap /// operations such as search, modify and add. /// In addition, LdapConnection objects allow you to bind to an /// Ldap server, set connection and search constraints, and perform /// several other tasks. /// An LdapConnection object is not connected on /// construction and can only be connected to one server at one /// port. /// /// Based on https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard. /// /// /// The following code describes how to use the LdapConnection class: /// /// /// class Example /// { /// using Unosquare.Swan; /// using Unosquare.Swan.Networking.Ldap; /// using System.Threading.Tasks; /// /// static async Task Main() /// { /// // create a LdapConnection object /// var connection = new LdapConnection(); /// /// // connect to a server /// await connection.Connect("ldap.forumsys.com", 389); /// /// // set up the credentials /// await connection.Bind("cn=read-only-admin,dc=example,dc=com", "password"); /// /// // retrieve all entries that have the specified email using ScopeSub /// // which searches all entries at all levels under /// // and including the specified base DN /// var searchResult = await connection /// .Search("dc=example,dc=com", LdapConnection.ScopeSub, "(cn=Isaac Newton)"); /// /// // if there are more entries remaining keep going /// while (searchResult.HasMore()) /// { /// // point to the next entry /// var entry = searchResult.Next(); /// /// // get all attributes /// var entryAttributes = entry.GetAttributeSet(); /// /// // select its name and print it out /// entryAttributes.GetAttribute("cn").StringValue.Info(); /// } /// /// // modify Tesla and sets its email as tesla@email.com /// connection.Modify("uid=tesla,dc=example,dc=com", /// new[] { /// new LdapModification(LdapModificationOp.Replace, /// "mail", "tesla@email.com") /// }); /// /// // delete the listed values from the given attribute /// connection.Modify("uid=tesla,dc=example,dc=com", /// new[] { /// new LdapModification(LdapModificationOp.Delete, /// "mail", "tesla@email.com") /// }); /// /// // add back the recently deleted property /// connection.Modify("uid=tesla,dc=example,dc=com", /// new[] { /// new LdapModification(LdapModificationOp.Add, /// "mail", "tesla@email.com") /// }); /// /// // disconnect from the LDAP server /// connection.Disconnect(); /// /// Terminal.Flush(); /// } /// } /// /// public class LdapConnection : IDisposable { private const int LdapV3 = 3; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private Connection _conn; private bool _isDisposing; /// /// Returns the protocol version uses to authenticate. /// 0 is returned if no authentication has been performed. /// /// /// The protocol version. /// public int ProtocolVersion => BindProperties?.ProtocolVersion ?? LdapV3; /// /// Returns the distinguished name (DN) used for as the bind name during /// the last successful bind operation. null is returned /// if no authentication has been performed or if the bind resulted in /// an anonymous connection. /// /// /// The authentication dn. /// public string AuthenticationDn => BindProperties == null ? null : (BindProperties.Anonymous ? null : BindProperties.AuthenticationDN); /// /// Returns the method used to authenticate the connection. The return /// value is one of the following:. /// /// /// /// The authentication method. /// public string AuthenticationMethod => BindProperties == null ? "simple" : BindProperties.AuthenticationMethod; /// /// Indicates whether the connection represented by this object is open /// at this time. /// /// /// True if connection is open; false if the connection is closed. /// public bool Connected => _conn?.IsConnected == true; internal BindProperties BindProperties { get; set; } internal List Messages { get; } = new List(); /// public void Dispose() { if (_isDisposing) return; _isDisposing = true; Disconnect(); _cts?.Dispose(); } /// /// Synchronously authenticates to the Ldap server (that the object is /// currently connected to) using the specified name, password, Ldap version, /// and constraints. /// If the object has been disconnected from an Ldap server, /// this method attempts to reconnect to the server. If the object /// has already authenticated, the old authentication is discarded. /// /// If non-null and non-empty, specifies that the /// connection and all operations through it should /// be authenticated with dn as the distinguished /// name. /// If non-null and non-empty, specifies that the /// connection and all operations through it should /// be authenticated with dn as the distinguished /// name and password. /// Note: the application should use care in the use /// of String password objects. These are long lived /// objects, and may expose a security risk, especially /// in objects that are serialized. The LdapConnection /// keeps no long lived instances of these objects. /// /// A representing the asynchronous operation. /// public Task Bind(string dn, string password) => Bind(LdapV3, dn, password); /// /// Synchronously authenticates to the Ldap server (that the object is /// currently connected to) using the specified name, password, Ldap version, /// and constraints. /// If the object has been disconnected from an Ldap server, /// this method attempts to reconnect to the server. If the object /// has already authenticated, the old authentication is discarded. /// /// The Ldap protocol version, use Ldap_V3. /// Ldap_V2 is not supported. /// If non-null and non-empty, specifies that the /// connection and all operations through it should /// be authenticated with dn as the distinguished /// name. /// If non-null and non-empty, specifies that the /// connection and all operations through it should /// be authenticated with dn as the distinguished /// name and passwd as password. /// Note: the application should use care in the use /// of String password objects. These are long lived /// objects, and may expose a security risk, especially /// in objects that are serialized. The LdapConnection /// keeps no long lived instances of these objects. /// A representing the asynchronous operation. public Task Bind(int version, string dn, string password) { dn = string.IsNullOrEmpty(dn) ? string.Empty : dn.Trim(); var passwordData = string.IsNullOrWhiteSpace(password) ? new sbyte[] { } : Encoding.UTF8.GetSBytes(password); var anonymous = false; if (passwordData.Length == 0) { anonymous = true; // anonymous, password length zero with simple bind dn = string.Empty; // set to null if anonymous } BindProperties = new BindProperties(version, dn, "simple", anonymous); return RequestLdapMessage(new LdapBindRequest(version, dn, passwordData)); } /// /// Connects to the specified host and port. /// If this LdapConnection object represents an open connection, the /// connection is closed first before the new connection is opened. /// At this point, there is no authentication, and any operations are /// conducted as an anonymous client. /// /// A host name or a dotted string representing the IP address /// of a host running an Ldap server. /// The TCP or UDP port number to connect to or contact. /// The default Ldap port is 389. /// A representing the asynchronous operation. public async Task Connect(string host, int port) { var tcpClient = new TcpClient(); await tcpClient.ConnectAsync(host, port).ConfigureAwait(false); _conn = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 0); #pragma warning disable 4014 Task.Run(() => RetrieveMessages(), _cts.Token); #pragma warning restore 4014 } /// /// Synchronously disconnects from the Ldap server. /// Before the object can perform Ldap operations again, it must /// reconnect to the server by calling connect. /// The disconnect method abandons any outstanding requests, issues an /// unbind request to the server, and then closes the socket. /// public void Disconnect() { // disconnect from API call _cts.Cancel(); _conn.Disconnect(); } /// /// Synchronously reads the entry for the specified distinguished name (DN), /// using the specified constraints, and retrieves only the specified /// attributes from the entry. /// /// The distinguished name of the entry to retrieve. /// The names of the attributes to retrieve. /// The cancellation token. /// /// the LdapEntry read from the server. /// /// Read response is ambiguous, multiple entries returned. public async Task Read(string dn, string[] attrs = null, CancellationToken ct = default) { var sr = await Search(dn, LdapScope.ScopeSub, null, attrs, false, ct); LdapEntry ret = null; if (sr.HasMore()) { ret = sr.Next(); if (sr.HasMore()) { throw new LdapException("Read response is ambiguous, multiple entries returned", LdapStatusCode.AmbiguousResponse); } } return ret; } /// /// Performs the search specified by the parameters, /// also allowing specification of constraints for the search (such /// as the maximum number of entries to find or the maximum time to /// wait for search results). /// /// The base distinguished name to search from. /// The scope of the entries to search. /// The search filter specifying the search criteria. /// The names of attributes to retrieve. /// If true, returns the names but not the values of /// the attributes found. If false, returns the /// names and values for attributes found. /// The cancellation token. /// /// A representing the asynchronous operation. /// public async Task Search( string @base, LdapScope scope, string filter = "objectClass=*", string[] attrs = null, bool typesOnly = false, CancellationToken ct = default) { // TODO: Add Search options var msg = new LdapSearchRequest(@base, scope, filter, attrs, 0, 1000, 0, typesOnly, null); await RequestLdapMessage(msg, ct).ConfigureAwait(false); return new LdapSearchResults(Messages, msg.MessageId); } /// /// Modifies the specified dn. /// /// Name of the distinguished. /// The mods. /// The cancellation token. /// /// A representing the asynchronous operation. /// /// distinguishedName. public Task Modify(string distinguishedName, LdapModification[] mods, CancellationToken ct = default) { if (distinguishedName == null) { throw new ArgumentNullException(nameof(distinguishedName)); } return RequestLdapMessage(new LdapModifyRequest(distinguishedName, mods, null), ct); } internal async Task RequestLdapMessage(LdapMessage msg, CancellationToken ct = default) { using (var stream = new MemoryStream()) { LberEncoder.Encode(msg.Asn1Object, stream); await _conn.WriteDataAsync(stream.ToArray(), true, ct).ConfigureAwait(false); try { while (new List(Messages).Any(x => x.MessageId == msg.MessageId) == false) await Task.Delay(100, ct).ConfigureAwait(false); } catch (ArgumentException) { // expected } var first = new List(Messages).FirstOrDefault(x => x.MessageId == msg.MessageId); if (first != null) { var response = new LdapResponse(first); response.ChkResultCode(); } } } internal void RetrieveMessages() { while (!_cts.IsCancellationRequested) { try { var asn1Id = new Asn1Identifier(_conn.ActiveStream); if (asn1Id.Tag != Asn1Sequence.Tag) { continue; // loop looking for an RfcLdapMessage identifier } // Turn the message into an RfcMessage class var asn1Len = new Asn1Length(_conn.ActiveStream); Messages.Add(new RfcLdapMessage(_conn.ActiveStream, asn1Len.Length)); } catch (IOException) { // ignore } } // ReSharper disable once FunctionNeverReturns } } }