382 lines
16 KiB
C#
382 lines
16 KiB
C#
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 {
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <example>
|
|
/// The following code describes how to use the LdapConnection class:
|
|
///
|
|
/// <code>
|
|
/// 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();
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
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 = "<Ausstehend>")]
|
|
private Connection _conn;
|
|
private Boolean _isDisposing;
|
|
|
|
/// <summary>
|
|
/// Returns the protocol version uses to authenticate.
|
|
/// 0 is returned if no authentication has been performed.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The protocol version.
|
|
/// </value>
|
|
public Int32 ProtocolVersion => this.BindProperties?.ProtocolVersion ?? LdapV3;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <value>
|
|
/// The authentication dn.
|
|
/// </value>
|
|
public String AuthenticationDn => this.BindProperties == null ? null : (this.BindProperties.Anonymous ? null : this.BindProperties.AuthenticationDN);
|
|
|
|
/// <summary>
|
|
/// Returns the method used to authenticate the connection. The return
|
|
/// value is one of the following:.
|
|
/// <ul><li>"none" indicates the connection is not authenticated.</li><li>
|
|
/// "simple" indicates simple authentication was used or that a null
|
|
/// or empty authentication DN was specified.
|
|
/// </li><li>"sasl" indicates that a SASL mechanism was used to authenticate</li></ul>
|
|
/// </summary>
|
|
/// <value>
|
|
/// The authentication method.
|
|
/// </value>
|
|
public String AuthenticationMethod => this.BindProperties == null ? "simple" : this.BindProperties.AuthenticationMethod;
|
|
|
|
/// <summary>
|
|
/// Indicates whether the connection represented by this object is open
|
|
/// at this time.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// True if connection is open; false if the connection is closed.
|
|
/// </returns>
|
|
public Boolean Connected => this._conn?.IsConnected == true;
|
|
|
|
internal BindProperties BindProperties {
|
|
get; set;
|
|
}
|
|
|
|
internal List<RfcLdapMessage> Messages { get; } = new List<RfcLdapMessage>();
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose() {
|
|
if(this._isDisposing) {
|
|
return;
|
|
}
|
|
|
|
this._isDisposing = true;
|
|
this.Disconnect();
|
|
this._cts?.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="dn">If non-null and non-empty, specifies that the
|
|
/// connection and all operations through it should
|
|
/// be authenticated with dn as the distinguished
|
|
/// name.</param>
|
|
/// <param name="password">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.</param>
|
|
/// <returns>
|
|
/// A <see cref="Task" /> representing the asynchronous operation.
|
|
/// </returns>
|
|
public Task Bind(String dn, String password) => this.Bind(LdapV3, dn, password);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="version">The Ldap protocol version, use Ldap_V3.
|
|
/// Ldap_V2 is not supported.</param>
|
|
/// <param name="dn">If non-null and non-empty, specifies that the
|
|
/// connection and all operations through it should
|
|
/// be authenticated with dn as the distinguished
|
|
/// name.</param>
|
|
/// <param name="password">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.</param>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="host">A host name or a dotted string representing the IP address
|
|
/// of a host running an Ldap server.</param>
|
|
/// <param name="port">The TCP or UDP port number to connect to or contact.
|
|
/// The default Ldap port is 389.</param>
|
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
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
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void Disconnect() {
|
|
// disconnect from API call
|
|
this._cts.Cancel();
|
|
this._conn.Disconnect();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synchronously reads the entry for the specified distinguished name (DN),
|
|
/// using the specified constraints, and retrieves only the specified
|
|
/// attributes from the entry.
|
|
/// </summary>
|
|
/// <param name="dn">The distinguished name of the entry to retrieve.</param>
|
|
/// <param name="attrs">The names of the attributes to retrieve.</param>
|
|
/// <param name="ct">The cancellation token.</param>
|
|
/// <returns>
|
|
/// the LdapEntry read from the server.
|
|
/// </returns>
|
|
/// <exception cref="LdapException">Read response is ambiguous, multiple entries returned.</exception>
|
|
public async Task<LdapEntry> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="base">The base distinguished name to search from.</param>
|
|
/// <param name="scope">The scope of the entries to search.</param>
|
|
/// <param name="filter">The search filter specifying the search criteria.</param>
|
|
/// <param name="attrs">The names of attributes to retrieve.</param>
|
|
/// <param name="typesOnly">If true, returns the names but not the values of
|
|
/// the attributes found. If false, returns the
|
|
/// names and values for attributes found.</param>
|
|
/// <param name="ct">The cancellation token.</param>
|
|
/// <returns>
|
|
/// A <see cref="Task" /> representing the asynchronous operation.
|
|
/// </returns>
|
|
public async Task<LdapSearchResults> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifies the specified dn.
|
|
/// </summary>
|
|
/// <param name="distinguishedName">Name of the distinguished.</param>
|
|
/// <param name="mods">The mods.</param>
|
|
/// <param name="ct">The cancellation token.</param>
|
|
/// <returns>
|
|
/// A <see cref="Task" /> representing the asynchronous operation.
|
|
/// </returns>
|
|
/// <exception cref="ArgumentNullException">distinguishedName.</exception>
|
|
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<RfcLdapMessage>(this.Messages).Any(x => x.MessageId == msg.MessageId) == false) {
|
|
await Task.Delay(100, ct).ConfigureAwait(false);
|
|
}
|
|
} catch(ArgumentException) {
|
|
// expected
|
|
}
|
|
|
|
RfcLdapMessage first = new List<RfcLdapMessage>(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
|
|
}
|
|
}
|
|
} |