using System; using System.Linq; using System.Collections.Generic; using System.Text; namespace Unosquare.Swan.Networking.Ldap { /// /// Represents a single entry in a directory, consisting of /// a distinguished name (DN) and zero or more attributes. /// An instance of /// LdapEntry is created in order to add an entry to a directory, and /// instances of LdapEntry are returned on a search by enumerating an /// LdapSearchResults. /// /// /// public class LdapEntry { private readonly LdapAttributeSet _attrs; /// /// Initializes a new instance of the class. /// Constructs a new entry with the specified distinguished name and set /// of attributes. /// /// The distinguished name of the new entry. The /// value is not validated. An invalid distinguished /// name will cause operations using this entry to fail. /// The initial set of attributes assigned to the /// entry. public LdapEntry(String dn = null, LdapAttributeSet attrs = null) { this.DN = dn ?? String.Empty; this._attrs = attrs ?? new LdapAttributeSet(); } /// /// Returns the distinguished name of the entry. /// /// /// The dn. /// public String DN { get; } /// /// Returns the attributes matching the specified attrName. /// /// The name of the attribute or attributes to return. /// /// The attribute matching the name. /// public LdapAttribute GetAttribute(String attrName) => this._attrs[attrName]; /// /// Returns the attribute set of the entry. /// All base and subtype variants of all attributes are /// returned. The LdapAttributeSet returned may be /// empty if there are no attributes in the entry. /// /// /// The attribute set of the entry. /// public LdapAttributeSet GetAttributeSet() => this._attrs; /// /// Returns an attribute set from the entry, consisting of only those /// attributes matching the specified subtypes. /// The getAttributeSet method can be used to extract only /// a particular language variant subtype of each attribute, /// if it exists. The "subtype" may be, for example, "lang-ja", "binary", /// or "lang-ja;phonetic". If more than one subtype is specified, separated /// with a semicolon, only those attributes with all of the named /// subtypes will be returned. The LdapAttributeSet returned may be /// empty if there are no matching attributes in the entry. /// /// One or more subtype specification(s), separated /// with semicolons. The "lang-ja" and /// "lang-en;phonetic" are valid subtype /// specifications. /// /// An attribute set from the entry with the attributes that /// match the specified subtypes or an empty set if no attributes /// match. /// public LdapAttributeSet GetAttributeSet(String subtype) => this._attrs.GetSubset(subtype); } /// /// The name and values of one attribute of a directory entry. /// LdapAttribute objects are used when searching for, adding, /// modifying, and deleting attributes from the directory. /// LdapAttributes are often used in conjunction with an /// LdapAttributeSet when retrieving or adding multiple /// attributes to an entry. /// public class LdapAttribute { private readonly String _baseName; // cn of cn;lang-ja;phonetic private readonly String[] _subTypes; // lang-ja of cn;lang-ja private Object[] _values; // Array of byte[] attribute values /// /// Initializes a new instance of the class. /// Constructs an attribute with no values. /// /// Name of the attribute. /// Attribute name cannot be null. public LdapAttribute(String attrName) { this.Name = attrName ?? throw new ArgumentNullException(nameof(attrName)); this._baseName = GetBaseName(attrName); this._subTypes = GetSubtypes(attrName); } /// /// Initializes a new instance of the class. /// Constructs an attribute with a single value. /// /// Name of the attribute. /// Value of the attribute as a string. /// Attribute value cannot be null. public LdapAttribute(String attrName, String attrString) : this(attrName) => this.Add(Encoding.UTF8.GetSBytes(attrString)); /// /// Returns the values of the attribute as an array of bytes. /// /// /// The byte value array. /// public SByte[][] ByteValueArray { get { if(this._values == null) { return new SByte[0][]; } Int32 size = this._values.Length; SByte[][] bva = new SByte[size][]; // Deep copy so application cannot change values for(Int32 i = 0, u = size; i < u; i++) { bva[i] = new SByte[((SByte[])this._values[i]).Length]; Array.Copy((Array)this._values[i], 0, bva[i], 0, bva[i].Length); } return bva; } } /// /// Returns the values of the attribute as an array of strings. /// /// /// The string value array. /// public String[] StringValueArray { get { if(this._values == null) { return new String[0]; } Int32 size = this._values.Length; String[] sva = new String[size]; for(Int32 j = 0; j < size; j++) { sva[j] = Encoding.UTF8.GetString((SByte[])this._values[j]); } return sva; } } /// /// Returns the the first value of the attribute as an UTF-8 string. /// /// /// The string value. /// public String StringValue => this._values == null ? null : Encoding.UTF8.GetString((SByte[])this._values[0]); /// /// Returns the first value of the attribute as a byte array or null. /// /// /// The byte value. /// public SByte[] ByteValue { get { if(this._values == null) { return null; } // Deep copy so app can't change the value SByte[] bva = new SByte[((SByte[])this._values[0]).Length]; Array.Copy((Array)this._values[0], 0, bva, 0, bva.Length); return bva; } } /// /// Returns the language subtype of the attribute, if any. /// For example, if the attribute name is cn;lang-ja;phonetic, /// this method returns the string, lang-ja. /// /// /// The language subtype. /// public String LangSubtype => this._subTypes?.FirstOrDefault(t => t.StartsWith("lang-")); /// /// Returns the name of the attribute. /// /// /// The name. /// public String Name { get; } internal String Value { set { this._values = null; this.Add(Encoding.UTF8.GetSBytes(value)); } } /// /// Extracts the subtypes from the specified attribute name. /// For example, if the attribute name is cn;lang-ja;phonetic, /// this method returns an array containing lang-ja and phonetic. /// /// Name of the attribute from which to extract /// the subtypes. /// /// An array subtypes or null if the attribute has none. /// /// Attribute name cannot be null. public static String[] GetSubtypes(String attrName) { if(attrName == null) { throw new ArgumentException("Attribute name cannot be null"); } Tokenizer st = new Tokenizer(attrName, ";"); String[] subTypes = null; Int32 cnt = st.Count; if(cnt > 0) { _ = st.NextToken(); // skip over basename subTypes = new String[cnt - 1]; Int32 i = 0; while(st.HasMoreTokens()) { subTypes[i++] = st.NextToken(); } } return subTypes; } /// /// Returns the base name of the specified attribute name. /// For example, if the attribute name is cn;lang-ja;phonetic, /// this method returns cn. /// /// Name of the attribute from which to extract the /// base name. /// The base name of the attribute. /// Attribute name cannot be null. public static String GetBaseName(String attrName) { if(attrName == null) { throw new ArgumentException("Attribute name cannot be null"); } Int32 idx = attrName.IndexOf(';'); return idx == -1 ? attrName : attrName.Substring(0, idx - 0); } /// /// Clones this instance. /// /// A cloned instance. public LdapAttribute Clone() { Object newObj = this.MemberwiseClone(); if(this._values != null) { Array.Copy(this._values, 0, ((LdapAttribute)newObj)._values, 0, this._values.Length); } return (LdapAttribute)newObj; } /// /// Adds a value to the attribute. /// /// Value of the attribute as a String. /// Attribute value cannot be null. public void AddValue(String attrString) { if(attrString == null) { throw new ArgumentException("Attribute value cannot be null"); } this.Add(Encoding.UTF8.GetSBytes(attrString)); } /// /// Adds a byte-formatted value to the attribute. /// /// Value of the attribute as raw bytes. /// Note: If attrBytes represents a string it should be UTF-8 encoded. /// Attribute value cannot be null. public void AddValue(SByte[] attrBytes) { if(attrBytes == null) { throw new ArgumentException("Attribute value cannot be null"); } this.Add(attrBytes); } /// /// Adds a base64 encoded value to the attribute. /// The value will be decoded and stored as bytes. String /// data encoded as a base64 value must be UTF-8 characters. /// /// The base64 value of the attribute as a String. /// Attribute value cannot be null. public void AddBase64Value(String attrString) { if(attrString == null) { throw new ArgumentException("Attribute value cannot be null"); } this.Add(Convert.FromBase64String(attrString).ToSByteArray()); } /// /// Adds a base64 encoded value to the attribute. /// The value will be decoded and stored as bytes. Character /// data encoded as a base64 value must be UTF-8 characters. /// /// The base64 value of the attribute as a StringBuffer. /// The start index of base64 encoded part, inclusive. /// The end index of base encoded part, exclusive. /// attrString. public void AddBase64Value(StringBuilder attrString, Int32 start, Int32 end) { if(attrString == null) { throw new ArgumentNullException(nameof(attrString)); } this.Add(Convert.FromBase64String(attrString.ToString(start, end)).ToSByteArray()); } /// /// Adds a base64 encoded value to the attribute. /// The value will be decoded and stored as bytes. Character /// data encoded as a base64 value must be UTF-8 characters. /// /// The base64 value of the attribute as an array of /// characters. /// attrChars. public void AddBase64Value(Char[] attrChars) { if(attrChars == null) { throw new ArgumentNullException(nameof(attrChars)); } this.Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray()); } /// /// Returns the base name of the attribute. /// For example, if the attribute name is cn;lang-ja;phonetic, /// this method returns cn. /// /// /// The base name of the attribute. /// public String GetBaseName() => this._baseName; /// /// Extracts the subtypes from the attribute name. /// For example, if the attribute name is cn;lang-ja;phonetic, /// this method returns an array containing lang-ja and phonetic. /// /// /// An array subtypes or null if the attribute has none. /// public String[] GetSubtypes() => this._subTypes; /// /// Reports if the attribute name contains the specified subtype. /// For example, if you check for the subtype lang-en and the /// attribute name is cn;lang-en, this method returns true. /// /// /// The single subtype to check for. /// /// /// True, if the attribute has the specified subtype; /// false, if it doesn't. /// public Boolean HasSubtype(String subtype) { if(subtype == null) { throw new ArgumentNullException(nameof(subtype)); } return this._subTypes != null && this._subTypes.Any(t => String.Equals(t, subtype, StringComparison.OrdinalIgnoreCase)); } /// /// Reports if the attribute name contains all the specified subtypes. /// For example, if you check for the subtypes lang-en and phonetic /// and if the attribute name is cn;lang-en;phonetic, this method /// returns true. If the attribute name is cn;phonetic or cn;lang-en, /// this method returns false. /// /// /// An array of subtypes to check for. /// /// /// True, if the attribute has all the specified subtypes; /// false, if it doesn't have all the subtypes. /// public Boolean HasSubtypes(String[] subtypes) { if(subtypes == null) { throw new ArgumentNullException(nameof(subtypes)); } for(Int32 i = 0; i < subtypes.Length; i++) { foreach(String sub in this._subTypes) { if(sub == null) { throw new ArgumentException($"subtype at array index {i} cannot be null"); } if(String.Equals(sub, subtypes[i], StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } /// /// Removes a string value from the attribute. /// /// Value of the attribute as a string. /// Note: Removing a value which is not present in the attribute has /// no effect. /// attrString. public void RemoveValue(String attrString) { if(attrString == null) { throw new ArgumentNullException(nameof(attrString)); } this.RemoveValue(Encoding.UTF8.GetSBytes(attrString)); } /// /// Removes a byte-formatted value from the attribute. /// /// Value of the attribute as raw bytes. /// Note: If attrBytes represents a string it should be UTF-8 encoded. /// Note: Removing a value which is not present in the attribute has /// no effect. /// /// attrBytes. public void RemoveValue(SByte[] attrBytes) { if(attrBytes == null) { throw new ArgumentNullException(nameof(attrBytes)); } for(Int32 i = 0; i < this._values.Length; i++) { if(!Equals(attrBytes, (SByte[])this._values[i])) { continue; } if(i == 0 && this._values.Length == 1) { // Optimize if first element of a single valued attr this._values = null; return; } if(this._values.Length == 1) { this._values = null; } else { Int32 moved = this._values.Length - i - 1; Object[] tmp = new Object[this._values.Length - 1]; if(i != 0) { Array.Copy(this._values, 0, tmp, 0, i); } if(moved != 0) { Array.Copy(this._values, i + 1, tmp, i, moved); } this._values = tmp; } break; } } /// /// Returns the number of values in the attribute. /// /// /// The number of values in the attribute. /// public Int32 Size() => this._values?.Length ?? 0; /// /// Compares this object with the specified object for order. /// Ordering is determined by comparing attribute names using the method Compare() of the String class. /// /// The LdapAttribute to be compared to this object. /// /// Returns a negative integer, zero, or a positive /// integer as this object is less than, equal to, or greater than the /// specified object. /// public Int32 CompareTo(Object attribute) => String.Compare(this.Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal); /// /// Returns a string representation of this LdapAttribute. /// /// /// a string representation of this LdapAttribute. /// /// NullReferenceException. public override String ToString() { StringBuilder result = new StringBuilder("LdapAttribute: "); _ = result.Append("{type='" + this.Name + "'"); if(this._values != null) { _ = result .Append(", ") .Append(this._values.Length == 1 ? "value='" : "values='"); for(Int32 i = 0; i < this._values.Length; i++) { if(i != 0) { _ = result.Append("','"); } if(((SByte[])this._values[i]).Length == 0) { continue; } String sval = Encoding.UTF8.GetString((SByte[])this._values[i]); if(sval.Length == 0) { // didn't decode well, must be binary _ = result.Append(" /// Adds an object to this object's list of attribute values. /// /// Ultimately all of this attribute's values are treated /// as binary data so we simplify the process by requiring /// that all data added to our list is in binary form. /// Note: If attrBytes represents a string it should be UTF-8 encoded. private void Add(SByte[] bytes) { if(this._values == null) { this._values = new Object[] { bytes }; } else { // Duplicate attribute values not allowed if(this._values.Any(t => Equals(bytes, (SByte[])t))) { return; // Duplicate, don't add } Object[] tmp = new Object[this._values.Length + 1]; Array.Copy(this._values, 0, tmp, 0, this._values.Length); tmp[this._values.Length] = bytes; this._values = tmp; } } private static Boolean Equals(SByte[] e1, SByte[] e2) { // If same object, they compare true if(e1 == e2) { return true; } // If either but not both are null, they compare false if(e1 == null || e2 == null) { return false; } // If arrays have different length, they compare false Int32 length = e1.Length; if(e2.Length != length) { return false; } // If any of the bytes are different, they compare false for(Int32 i = 0; i < length; i++) { if(e1[i] != e2[i]) { return false; } } return true; } } /// /// A set of LdapAttribute objects. /// An LdapAttributeSet is a collection of LdapAttribute /// classes as returned from an LdapEntry on a search or read /// operation. LdapAttributeSet may be also used to construct an entry /// to be added to a directory. /// /// /// public class LdapAttributeSet : Dictionary { /// /// Initializes a new instance of the class. /// public LdapAttributeSet() : base(StringComparer.OrdinalIgnoreCase) { // placeholder } /// /// Creates a new attribute set containing only the attributes that have /// the specified subtypes. /// For example, suppose an attribute set contains the following /// attributes: ///
  • cn
  • cn;lang-ja
  • sn;phonetic;lang-ja
  • sn;lang-us
/// Calling the getSubset method and passing lang-ja as the /// argument, the method returns an attribute set containing the following /// attributes:. ///
  • cn;lang-ja
  • sn;phonetic;lang-ja
///
/// Semi-colon delimited list of subtypes to include. For /// example: /// /// Note: Novell eDirectory does not currently support language subtypes. /// It does support the "binary" subtype. /// /// An attribute set containing the attributes that match the /// specified subtype. /// public LdapAttributeSet GetSubset(String subtype) { // Create a new tempAttributeSet LdapAttributeSet tempAttributeSet = new LdapAttributeSet(); foreach(KeyValuePair kvp in this) { if(kvp.Value.HasSubtype(subtype)) { _ = tempAttributeSet.Add(kvp.Value.Clone()); } } return tempAttributeSet; } /// /// Returns true if this set contains an attribute of the same name /// as the specified attribute. /// /// Object of type LdapAttribute. /// /// true if this set contains the specified attribute. /// public Boolean Contains(Object attr) => this.ContainsKey(((LdapAttribute)attr).Name); /// /// Adds the specified attribute to this set if it is not already present. /// If an attribute with the same name already exists in the set then the /// specified attribute will not be added. /// /// Object of type LdapAttribute. /// /// true if the attribute was added. /// public Boolean Add(LdapAttribute attr) { String name = attr.Name; if(this.ContainsKey(name)) { return false; } this[name] = attr; return true; } /// /// Removes the specified object from this set if it is present. /// If the specified object is of type LdapAttribute, the /// specified attribute will be removed. If the specified object is of type /// string, the attribute with a name that matches the string will /// be removed. /// /// The entry. /// /// true if the object was removed. /// public Boolean Remove(LdapAttribute entry) => this.Remove(entry.Name); /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// public override String ToString() { StringBuilder retValue = new StringBuilder("LdapAttributeSet: "); Boolean first = true; foreach(KeyValuePair attr in this) { if(!first) { _ = retValue.Append(" "); } first = false; _ = retValue.Append(attr); } return retValue.ToString(); } } }