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 Unosquare.Swan.Exceptions; namespace Unosquare.Swan.Networking.Ldap { /// /// 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 Int32 LdapV3 = 3; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); [System.Diagnostics.CodeAnalysis.SuppressMessage("Codequalität", "IDE0069:Verwerfbare Felder verwerfen", Justification = "")] private Connection _conn; private Boolean _isDisposing; /// /// Returns the protocol version uses to authenticate. /// 0 is returned if no authentication has been performed. /// /// /// The protocol version. /// public Int32 ProtocolVersion => this.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 => this.BindProperties == null ? null : (this.BindProperties.Anonymous ? null : this.BindProperties.AuthenticationDN); /// /// Returns the method used to authenticate the connection. The return /// value is one of the following:. /// /// /// /// The authentication method. /// public String AuthenticationMethod => this.BindProperties == null ? "simple" : this.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 Boolean Connected => this._conn?.IsConnected == true; internal BindProperties BindProperties { get; set; } internal List Messages { get; } = new List(); /// public void Dispose() { if(this._isDisposing) { return; } this._isDisposing = true; this.Disconnect(); this._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) => this.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(Int32 version, String dn, String password) { dn = String.IsNullOrEmpty(dn) ? String.Empty : dn.Trim(); SByte[] passwordData = String.IsNullOrWhiteSpace(password) ? new SByte[] { } : Encoding.UTF8.GetSBytes(password); Boolean anonymous = false; if(passwordData.Length == 0) { anonymous = true; // anonymous, password length zero with simple bind dn = String.Empty; // set to null if anonymous } this.BindProperties = new BindProperties(version, dn, "simple", anonymous); return this.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, Int32 port) { TcpClient tcpClient = new TcpClient(); await tcpClient.ConnectAsync(host, port).ConfigureAwait(false); this._conn = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 0); #pragma warning disable 4014 _ = Task.Run(() => this.RetrieveMessages(), this._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 this._cts.Cancel(); this._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) { LdapSearchResults sr = await this.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, Boolean typesOnly = false, CancellationToken ct = default) { // TODO: Add Search options LdapSearchRequest msg = new LdapSearchRequest(@base, scope, filter, attrs, 0, 1000, 0, typesOnly, null); await this.RequestLdapMessage(msg, ct).ConfigureAwait(false); return new LdapSearchResults(this.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 this.RequestLdapMessage(new LdapModifyRequest(distinguishedName, mods, null), ct); } internal async Task RequestLdapMessage(LdapMessage msg, CancellationToken ct = default) { using(MemoryStream stream = new MemoryStream()) { LberEncoder.Encode(msg.Asn1Object, stream); await this._conn.WriteDataAsync(stream.ToArray(), true, ct).ConfigureAwait(false); try { while(new List(this.Messages).Any(x => x.MessageId == msg.MessageId) == false) { await Task.Delay(100, ct).ConfigureAwait(false); } } catch(ArgumentException) { // expected } RfcLdapMessage first = new List(this.Messages).FirstOrDefault(x => x.MessageId == msg.MessageId); if(first != null) { LdapResponse response = new LdapResponse(first); response.ChkResultCode(); } } } internal void RetrieveMessages() { while(!this._cts.IsCancellationRequested) { try { Asn1Identifier asn1Id = new Asn1Identifier(this._conn.ActiveStream); if(asn1Id.Tag != Asn1Sequence.Tag) { continue; // loop looking for an RfcLdapMessage identifier } // Turn the message into an RfcMessage class Asn1Length asn1Len = new Asn1Length(this._conn.ActiveStream); this.Messages.Add(new RfcLdapMessage(this._conn.ActiveStream, asn1Len.Length)); } catch(IOException) { // ignore } } // ReSharper disable once FunctionNeverReturns } } }