namespace Unosquare.Swan.Networking.Ldap { using System.Linq; using System; using System.Collections.Generic; using System.Text; /// /// 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) { DN = dn ?? string.Empty; _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) => _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() => _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) => _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) { Name = attrName ?? throw new ArgumentNullException(nameof(attrName)); _baseName = GetBaseName(attrName); _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) { 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 (_values == null) return new sbyte[0][]; var size = _values.Length; var bva = new sbyte[size][]; // Deep copy so application cannot change values for (int i = 0, u = size; i < u; i++) { bva[i] = new sbyte[((sbyte[])_values[i]).Length]; Array.Copy((Array)_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 (_values == null) return new string[0]; var size = _values.Length; var sva = new string[size]; for (var j = 0; j < size; j++) { sva[j] = Encoding.UTF8.GetString((sbyte[])_values[j]); } return sva; } } /// /// Returns the the first value of the attribute as an UTF-8 string. /// /// /// The string value. /// public string StringValue => _values == null ? null : Encoding.UTF8.GetString((sbyte[])_values[0]); /// /// Returns the first value of the attribute as a byte array or null. /// /// /// The byte value. /// public sbyte[] ByteValue { get { if (_values == null) return null; // Deep copy so app can't change the value var bva = new sbyte[((sbyte[])_values[0]).Length]; Array.Copy((Array)_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 => _subTypes?.FirstOrDefault(t => t.StartsWith("lang-")); /// /// Returns the name of the attribute. /// /// /// The name. /// public string Name { get; } internal string Value { set { _values = null; 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"); } var st = new Tokenizer(attrName, ";"); string[] subTypes = null; var cnt = st.Count; if (cnt > 0) { st.NextToken(); // skip over basename subTypes = new string[cnt - 1]; var 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"); } var idx = attrName.IndexOf(';'); return idx == -1 ? attrName : attrName.Substring(0, idx - 0); } /// /// Clones this instance. /// /// A cloned instance. public LdapAttribute Clone() { var newObj = MemberwiseClone(); if (_values != null) { Array.Copy(_values, 0, ((LdapAttribute)newObj)._values, 0, _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"); } 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"); } 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"); } 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, int start, int end) { if (attrString == null) { throw new ArgumentNullException(nameof(attrString)); } 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)); } 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() => _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() => _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 bool HasSubtype(string subtype) { if (subtype == null) { throw new ArgumentNullException(nameof(subtype)); } return _subTypes != null && _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 bool HasSubtypes(string[] subtypes) { if (subtypes == null) { throw new ArgumentNullException(nameof(subtypes)); } for (var i = 0; i < subtypes.Length; i++) { foreach (var sub in _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)); } 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 (var i = 0; i < _values.Length; i++) { if (!Equals(attrBytes, (sbyte[])_values[i])) continue; if (i == 0 && _values.Length == 1) { // Optimize if first element of a single valued attr _values = null; return; } if (_values.Length == 1) { _values = null; } else { var moved = _values.Length - i - 1; var tmp = new object[_values.Length - 1]; if (i != 0) { Array.Copy(_values, 0, tmp, 0, i); } if (moved != 0) { Array.Copy(_values, i + 1, tmp, i, moved); } _values = tmp; } break; } } /// /// Returns the number of values in the attribute. /// /// /// The number of values in the attribute. /// public int Size() => _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 int CompareTo(object attribute) => string.Compare(Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal); /// /// Returns a string representation of this LdapAttribute. /// /// /// a string representation of this LdapAttribute. /// /// NullReferenceException. public override string ToString() { var result = new StringBuilder("LdapAttribute: "); result.Append("{type='" + Name + "'"); if (_values != null) { result .Append(", ") .Append(_values.Length == 1 ? "value='" : "values='"); for (var i = 0; i < _values.Length; i++) { if (i != 0) { result.Append("','"); } if (((sbyte[])_values[i]).Length == 0) { continue; } var sval = Encoding.UTF8.GetString((sbyte[])_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 (_values == null) { _values = new object[] { bytes }; } else { // Duplicate attribute values not allowed if (_values.Any(t => Equals(bytes, (sbyte[])t))) { return; // Duplicate, don't add } var tmp = new object[_values.Length + 1]; Array.Copy(_values, 0, tmp, 0, _values.Length); tmp[_values.Length] = bytes; _values = tmp; } } private static bool 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 var length = e1.Length; if (e2.Length != length) return false; // If any of the bytes are different, they compare false for (var 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 var tempAttributeSet = new LdapAttributeSet(); foreach (var 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 bool Contains(object attr) => 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 bool Add(LdapAttribute attr) { var name = attr.Name; if (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 bool Remove(LdapAttribute entry) => Remove(entry.Name); /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// public override string ToString() { var retValue = new StringBuilder("LdapAttributeSet: "); var first = true; foreach (var attr in this) { if (!first) { retValue.Append(" "); } first = false; retValue.Append(attr); } return retValue.ToString(); } } }