Init RaspberryIO

This commit is contained in:
2019-02-17 14:08:57 +01:00
commit e7c3f7af91
193 changed files with 36743 additions and 0 deletions
+621
View File
@@ -0,0 +1,621 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System;
using System.IO;
using System.Text;
/// <summary>
/// The Asn1Set class can hold an unordered collection of components with
/// identical type. This class inherits from the Asn1Structured class
/// which already provides functionality to hold multiple Asn1 components.
/// </summary>
/// <seealso cref="Asn1Structured" />
internal class Asn1SetOf
: Asn1Structured
{
public const int Tag = 0x11;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag);
public Asn1SetOf(int size = 10)
: base(Id, size)
{
}
public override string ToString() => ToString("SET OF: { ");
}
/// <summary>
/// The Asn1Choice object represents the choice of any Asn1Object. All
/// Asn1Object methods are delegated to the object this Asn1Choice contains.
/// </summary>
/// <seealso cref="Asn1Object" />
internal class Asn1Choice
: Asn1Object
{
private Asn1Object _content;
public Asn1Choice(Asn1Object content = null)
{
_content = content;
}
protected internal virtual Asn1Object ChoiceValue
{
get => _content;
set => _content = value;
}
public override Asn1Identifier GetIdentifier() => _content.GetIdentifier();
public override void SetIdentifier(Asn1Identifier id) => _content.SetIdentifier(id);
public override string ToString() => _content.ToString();
}
/// <summary>
/// This class is used to encapsulate an ASN.1 Identifier.
/// An Asn1Identifier is composed of three parts:
/// <li> a class type,</li><li> a form, and</li><li> a tag.</li>
/// The class type is defined as:
/// <pre>
/// bit 8 7 TAG CLASS
/// ------- -----------
/// 0 0 UNIVERSAL
/// 0 1 APPLICATION
/// 1 0 CONTEXT
/// 1 1 PRIVATE
/// </pre>
/// The form is defined as:
/// <pre>
/// bit 6 FORM
/// ----- --------
/// 0 PRIMITIVE
/// 1 CONSTRUCTED
/// </pre>
/// Note: CONSTRUCTED types are made up of other CONSTRUCTED or PRIMITIVE
/// types.
/// The tag is defined as:.
/// <pre>
/// bit 5 4 3 2 1 TAG
/// ------------- ---------------------------------------------
/// 0 0 0 0 0
/// . . . . .
/// 1 1 1 1 0 (0-30) single octet tag
/// 1 1 1 1 1 (&gt; 30) multiple octet tag, more octets follow
/// </pre></summary>
internal sealed class Asn1Identifier
{
public Asn1Identifier(Asn1IdentifierTag tagClass, bool constructed, int tag)
{
Asn1Class = tagClass;
Constructed = constructed;
Tag = tag;
}
public Asn1Identifier(LdapOperation tag)
: this(Asn1IdentifierTag.Application, true, (int) tag)
{
}
public Asn1Identifier(int contextTag, bool constructed = false)
: this(Asn1IdentifierTag.Context, constructed, contextTag)
{
}
public Asn1Identifier(Stream stream)
{
var r = stream.ReadByte();
EncodedLength++;
if (r < 0)
throw new EndOfStreamException("BERDecoder: decode: EOF in Identifier");
Asn1Class = (Asn1IdentifierTag) (r >> 6);
Constructed = (r & 0x20) != 0;
Tag = r & 0x1F; // if tag < 30 then its a single octet identifier.
if (Tag == 0x1F)
{
// if true, its a multiple octet identifier.
Tag = DecodeTagNumber(stream);
}
}
public Asn1IdentifierTag Asn1Class { get; }
public bool Constructed { get; }
public int Tag { get; }
public int EncodedLength { get; private set; }
public bool Universal => Asn1Class == Asn1IdentifierTag.Universal;
public object Clone() => MemberwiseClone();
private int DecodeTagNumber(Stream stream)
{
var n = 0;
while (true)
{
var r = stream.ReadByte();
EncodedLength++;
if (r < 0)
throw new EndOfStreamException("BERDecoder: decode: EOF in tag number");
n = (n << 7) + (r & 0x7F);
if ((r & 0x80) == 0) break;
}
return n;
}
}
/// <summary>
/// This is the base class for all other Asn1 types.
/// </summary>
internal abstract class Asn1Object
{
private static readonly string[] ClassTypes = {"[UNIVERSAL ", "[APPLICATION ", "[", "[PRIVATE "};
private Asn1Identifier _id;
protected Asn1Object(Asn1Identifier id = null)
{
_id = id;
}
public virtual Asn1Identifier GetIdentifier() => _id;
public virtual void SetIdentifier(Asn1Identifier identifier) => _id = identifier;
public override string ToString()
{
var identifier = GetIdentifier();
return $"{ClassTypes[(int) identifier.Asn1Class]}{identifier.Tag}]";
}
}
/// <summary>
/// This class encapsulates the OCTET STRING type.
/// </summary>
/// <seealso cref="Asn1Object" />
internal sealed class Asn1OctetString
: Asn1Object
{
public const int Tag = 0x04;
private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag);
private readonly sbyte[] _content;
public Asn1OctetString(sbyte[] content)
: base(Id)
{
_content = content;
}
public Asn1OctetString(string content)
: base(Id)
{
_content = Encoding.UTF8.GetSBytes(content);
}
public Asn1OctetString(Stream stream, int len)
: base(Id)
{
_content = len > 0 ? (sbyte[]) LberDecoder.DecodeOctetString(stream, len) : new sbyte[0];
}
public sbyte[] ByteValue() => _content;
public string StringValue() => Encoding.UTF8.GetString(_content);
public override string ToString() => base.ToString() + "OCTET STRING: " + StringValue();
}
/// <summary>
/// The Asn1Tagged class can hold a base Asn1Object with a distinctive tag
/// describing the type of that base object. It also maintains a boolean value
/// indicating whether the value should be encoded by EXPLICIT or IMPLICIT
/// means. (Explicit is true by default.)
/// If the type is encoded IMPLICITLY, the base types form, length and content
/// will be encoded as usual along with the class type and tag specified in
/// the constructor of this Asn1Tagged class.
/// If the type is to be encoded EXPLICITLY, the base type will be encoded as
/// usual after the Asn1Tagged identifier has been encoded.
/// </summary>
/// <seealso cref="Asn1Object" />
internal class Asn1Tagged : Asn1Object
{
private Asn1Object _content;
public Asn1Tagged(Asn1Identifier identifier, Asn1Object obj = null, bool isExplicit = true)
: base(identifier)
{
_content = obj;
Explicit = isExplicit;
if (!isExplicit)
{
// replace object's id with new tag.
_content?.SetIdentifier(identifier);
}
}
public Asn1Tagged(Asn1Identifier identifier, sbyte[] vals)
: base(identifier)
{
_content = new Asn1OctetString(vals);
Explicit = false;
}
public Asn1Tagged(Stream stream, int len, Asn1Identifier identifier)
: base(identifier)
{
// If we are decoding an implicit tag, there is no way to know at this
// low level what the base type really is. We can place the content
// into an Asn1OctetString type and pass it back to the application who
// will be able to create the appropriate ASN.1 type for this tag.
_content = new Asn1OctetString(stream, len);
}
public Asn1Object TaggedValue
{
get => _content;
set
{
_content = value;
if (!Explicit)
{
// replace object's id with new tag.
value?.SetIdentifier(GetIdentifier());
}
}
}
public bool Explicit { get; }
public override string ToString() => Explicit ? base.ToString() + _content : _content.ToString();
}
/// <summary>
/// This class serves as the base type for all ASN.1
/// structured types.
/// </summary>
/// <seealso cref="Asn1Object" />
internal abstract class Asn1Structured : Asn1Object
{
private Asn1Object[] _content;
private int _contentIndex;
protected internal Asn1Structured(Asn1Identifier id, int size = 10)
: base(id)
{
_content = new Asn1Object[size];
}
public Asn1Object[] ToArray()
{
var cloneArray = new Asn1Object[_contentIndex];
Array.Copy(_content, 0, cloneArray, 0, _contentIndex);
return cloneArray;
}
public void Add(string s) => Add(new Asn1OctetString(s));
public void Add(Asn1Object obj)
{
if (_contentIndex == _content.Length)
{
// Array too small, need to expand it, double length
var newArray = new Asn1Object[_contentIndex + _contentIndex];
Array.Copy(_content, 0, newArray, 0, _contentIndex);
_content = newArray;
}
_content[_contentIndex++] = obj;
}
public void Set(int index, Asn1Object value)
{
if (index >= _contentIndex || index < 0)
{
throw new IndexOutOfRangeException($"Asn1Structured: get: index {index}, size {_contentIndex}");
}
_content[index] = value;
}
public Asn1Object Get(int index)
{
if (index >= _contentIndex || index < 0)
{
throw new IndexOutOfRangeException($"Asn1Structured: set: index {index}, size {_contentIndex}");
}
return _content[index];
}
public int Size() => _contentIndex;
public string ToString(string type)
{
var sb = new StringBuilder().Append(type);
for (var i = 0; i < _contentIndex; i++)
{
sb.Append(_content[i]);
if (i != _contentIndex - 1)
sb.Append(", ");
}
sb.Append(" }");
return base.ToString() + sb;
}
protected internal void DecodeStructured(Stream stream, int len)
{
var componentLen = new int[1]; // collects length of component
while (len > 0)
{
Add(LberDecoder.Decode(stream, componentLen));
len -= componentLen[0];
}
}
}
/// <summary>
/// This class encapsulates the ASN.1 BOOLEAN type.
/// </summary>
/// <seealso cref="Asn1Object" />
internal class Asn1Boolean
: Asn1Object
{
public const int Tag = 0x01;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag);
private readonly bool _content;
public Asn1Boolean(bool content)
: base(Id)
{
_content = content;
}
public Asn1Boolean(Stream stream, int len)
: base(Id)
{
_content = LberDecoder.DecodeBoolean(stream, len);
}
public bool BooleanValue() => _content;
public override string ToString() => $"{base.ToString()}BOOLEAN: {_content}";
}
/// <summary>
/// This class represents the ASN.1 NULL type.
/// </summary>
/// <seealso cref="Asn1Object" />
internal sealed class Asn1Null
: Asn1Object
{
public const int Tag = 0x05;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag);
public Asn1Null()
: base(Id)
{
}
public override string ToString() => $"{base.ToString()}NULL: \"\"";
}
/// <summary>
/// This abstract class is the base class
/// for all Asn1 numeric (integral) types. These include
/// Asn1Integer and Asn1Enumerated.
/// </summary>
/// <seealso cref="Asn1Object" />
internal abstract class Asn1Numeric : Asn1Object
{
private readonly long _content;
internal Asn1Numeric(Asn1Identifier id, int numericValue)
: base(id)
{
_content = numericValue;
}
internal Asn1Numeric(Asn1Identifier id, long numericValue)
: base(id)
{
_content = numericValue;
}
public int IntValue() => (int) _content;
public long LongValue() => _content;
}
/// <summary>
/// This class provides a means to manipulate ASN.1 Length's. It will
/// be used by Asn1Encoder's and Asn1Decoder's by composition.
/// </summary>
internal sealed class Asn1Length
{
public Asn1Length(Stream stream)
{
var r = stream.ReadByte();
EncodedLength++;
if (r == 0x80)
{
Length = -1;
}
else if (r < 0x80)
{
Length = r;
}
else
{
Length = 0;
for (r = r & 0x7F; r > 0; r--)
{
var part = stream.ReadByte();
EncodedLength++;
if (part < 0)
throw new EndOfStreamException("BERDecoder: decode: EOF in Asn1Length");
Length = (Length << 8) + part;
}
}
}
public int Length { get; }
public int EncodedLength { get; }
}
/// <summary>
/// The Asn1Sequence class can hold an ordered collection of components with
/// distinct type.
/// This class inherits from the Asn1Structured class which
/// provides functionality to hold multiple Asn1 components.
/// </summary>
/// <seealso cref="Asn1Structured" />
internal class Asn1Sequence
: Asn1Structured
{
public const int Tag = 0x10;
private static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag);
public Asn1Sequence(int size)
: base(Id, size)
{
}
public Asn1Sequence(Stream stream, int len)
: base(Id)
{
DecodeStructured(stream, len);
}
public override string ToString() => ToString("SEQUENCE: { ");
}
/// <summary>
/// The Asn1Set class can hold an unordered collection of components with
/// distinct type. This class inherits from the Asn1Structured class
/// which already provides functionality to hold multiple Asn1 components.
/// </summary>
/// <seealso cref="Asn1Structured" />
internal sealed class Asn1Set
: Asn1Structured
{
public const int Tag = 0x11;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag);
public Asn1Set(Stream stream, int len)
: base(Id)
{
DecodeStructured(stream, len);
}
public override string ToString() => ToString("SET: { ");
}
/// <summary>
/// This class encapsulates the ASN.1 INTEGER type.
/// </summary>
/// <seealso cref="Asn1Numeric" />
internal class Asn1Integer
: Asn1Numeric
{
public const int Tag = 0x02;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag);
public Asn1Integer(int content)
: base(Id, content)
{
}
public Asn1Integer(Stream stream, int len)
: base(Id, LberDecoder.DecodeNumeric(stream, len))
{
}
public override string ToString() => base.ToString() + "INTEGER: " + LongValue();
}
/// <summary>
/// This class encapsulates the ASN.1 ENUMERATED type.
/// </summary>
/// <seealso cref="Asn1Numeric" />
internal sealed class Asn1Enumerated : Asn1Numeric
{
public const int Tag = 0x0a;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, false, Tag);
public Asn1Enumerated(LdapScope content)
: base(Id, (int) content)
{
}
public Asn1Enumerated(int content)
: base(Id, content)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Asn1Enumerated"/> class.
/// Constructs an Asn1Enumerated object by decoding data from an
/// input stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">The length.</param>
public Asn1Enumerated(Stream stream, int len)
: base(Id, LberDecoder.DecodeNumeric(stream, len))
{
}
public override string ToString() => base.ToString() + "ENUMERATED: " + LongValue();
}
/// <summary>
/// The Asn1SequenceOf class is used to hold an ordered collection
/// of components with identical type. This class inherits
/// from the Asn1Structured class which already provides
/// functionality to hold multiple Asn1 components.
/// </summary>
/// <seealso cref="Asn1Structured" />
internal class Asn1SequenceOf : Asn1Structured
{
public const int Tag = 0x10;
public static readonly Asn1Identifier Id = new Asn1Identifier(Asn1IdentifierTag.Universal, true, Tag);
public Asn1SequenceOf(int size)
: base(Id, size)
{
}
public Asn1SequenceOf(Stream stream, int len)
: base(Id)
{
DecodeStructured(stream, len);
}
public override string ToString() => ToString("SEQUENCE OF: { ");
}
}
@@ -0,0 +1,166 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.IO;
/// <summary>
/// This class provides LBER decoding routines for ASN.1 Types. LBER is a
/// subset of BER as described in the following taken from 5.1 of RFC 2251:
/// 5.1. Mapping Onto BER-based Transport Services
/// The protocol elements of Ldap are encoded for exchange using the
/// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the
/// high overhead involved in using certain elements of the BER, the
/// following additional restrictions are placed on BER-encodings of Ldap
/// protocol elements:
/// <li>(1) Only the definite form of length encoding will be used.</li>
/// <li>(2) OCTET STRING values will be encoded in the primitive form only.</li><li>
/// (3) If the value of a BOOLEAN type is true, the encoding MUST have
/// its contents octets set to hex "FF".
/// </li><li>
/// (4) If a value of a type is its default value, it MUST be absent.
/// Only some BOOLEAN and INTEGER types have default values in this
/// protocol definition.
/// These restrictions do not apply to ASN.1 types encapsulated inside of
/// OCTET STRING values, such as attribute values, unless otherwise
/// noted.
/// </li>
/// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) -
/// Specification of Basic Notation", 1994.
/// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic,
/// Canonical, and Distinguished Encoding Rules", 1994.
/// </summary>
internal static class LberDecoder
{
/// <summary>
/// Decode an LBER encoded value into an Asn1Object from an InputStream.
/// This method also returns the total length of this encoded
/// Asn1Object (length of type + length of length + length of content)
/// in the parameter len. This information is helpful when decoding
/// structured types.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">The length.</param>
/// <returns>
/// Decoded Asn1Obect.
/// </returns>
/// <exception cref="EndOfStreamException">Unknown tag.</exception>
public static Asn1Object Decode(Stream stream, int[] len)
{
var asn1Id = new Asn1Identifier(stream);
var asn1Len = new Asn1Length(stream);
var length = asn1Len.Length;
len[0] = asn1Id.EncodedLength + asn1Len.EncodedLength + length;
if (asn1Id.Universal == false)
return new Asn1Tagged(stream, length, (Asn1Identifier) asn1Id.Clone());
switch (asn1Id.Tag)
{
case Asn1Sequence.Tag:
return new Asn1Sequence(stream, length);
case Asn1Set.Tag:
return new Asn1Set(stream, length);
case Asn1Boolean.Tag:
return new Asn1Boolean(stream, length);
case Asn1Integer.Tag:
return new Asn1Integer(stream, length);
case Asn1OctetString.Tag:
return new Asn1OctetString(stream, length);
case Asn1Enumerated.Tag:
return new Asn1Enumerated(stream, length);
case Asn1Null.Tag:
return new Asn1Null(); // has no content to decode.
default:
throw new EndOfStreamException("Unknown tag");
}
}
/// <summary>
/// Decode a boolean directly from a stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">Length in bytes.</param>
/// <returns>
/// Decoded boolean object.
/// </returns>
/// <exception cref="EndOfStreamException">LBER: BOOLEAN: decode error: EOF.</exception>
public static bool DecodeBoolean(Stream stream, int len)
{
var lber = new sbyte[len];
if (stream.ReadInput(ref lber, 0, lber.Length) != len)
throw new EndOfStreamException("LBER: BOOLEAN: decode error: EOF");
return lber[0] != 0x00;
}
/// <summary>
/// Decode a Numeric type directly from a stream. Decodes INTEGER
/// and ENUMERATED types.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">Length in bytes.</param>
/// <returns>
/// Decoded numeric object.
/// </returns>
/// <exception cref="EndOfStreamException">
/// LBER: NUMERIC: decode error: EOF
/// or
/// LBER: NUMERIC: decode error: EOF.
/// </exception>
public static long DecodeNumeric(Stream stream, int len)
{
long l = 0;
var r = stream.ReadByte();
if (r < 0)
throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF");
if ((r & 0x80) != 0)
{
// check for negative number
l = -1;
}
l = (l << 8) | r;
for (var i = 1; i < len; i++)
{
r = stream.ReadByte();
if (r < 0)
throw new EndOfStreamException("LBER: NUMERIC: decode error: EOF");
l = (l << 8) | r;
}
return l;
}
/// <summary>
/// Decode an OctetString directly from a stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">Length in bytes.</param>
/// <returns>Decoded octet. </returns>
public static object DecodeOctetString(Stream stream, int len)
{
var octets = new sbyte[len];
var totalLen = 0;
while (totalLen < len)
{
// Make sure we have read all the data
totalLen += stream.ReadInput(ref octets, totalLen, len - totalLen);
}
return octets;
}
}
}
@@ -0,0 +1,246 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.IO;
/// <summary>
/// This class provides LBER encoding routines for ASN.1 Types. LBER is a
/// subset of BER as described in the following taken from 5.1 of RFC 2251:
/// 5.1. Mapping Onto BER-based Transport Services
/// The protocol elements of Ldap are encoded for exchange using the
/// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the
/// high overhead involved in using certain elements of the BER, the
/// following additional restrictions are placed on BER-encodings of Ldap
/// protocol elements:
/// <li>(1) Only the definite form of length encoding will be used.</li>
/// <li>(2) OCTET STRING values will be encoded in the primitive form only.</li><li>
/// (3) If the value of a BOOLEAN type is true, the encoding MUST have
/// its contents octets set to hex "FF".
/// </li><li>
/// (4) If a value of a type is its default value, it MUST be absent.
/// Only some BOOLEAN and INTEGER types have default values in this
/// protocol definition.
/// These restrictions do not apply to ASN.1 types encapsulated inside of
/// OCTET STRING values, such as attribute values, unless otherwise
/// noted.
/// </li>
/// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) -
/// Specification of Basic Notation", 1994.
/// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic,
/// Canonical, and Distinguished Encoding Rules", 1994.
/// </summary>
internal static class LberEncoder
{
/// <summary>
/// BER Encode an Asn1Boolean directly into the specified output stream.
/// </summary>
/// <param name="b">The Asn1Boolean object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1Boolean b, Stream stream)
{
Encode(b.GetIdentifier(), stream);
stream.WriteByte(0x01);
stream.WriteByte((byte) (b.BooleanValue() ? 0xff : 0x00));
}
/// <summary>
/// Encode an Asn1Numeric directly into the specified outputstream.
/// Use a two's complement representation in the fewest number of octets
/// possible.
/// Can be used to encode INTEGER and ENUMERATED values.
/// </summary>
/// <param name="n">The Asn1Numeric object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1Numeric n, Stream stream)
{
var octets = new sbyte[8];
sbyte len;
var longValue = n.LongValue();
long endValue = longValue < 0 ? -1 : 0;
var endSign = endValue & 0x80;
for (len = 0; len == 0 || longValue != endValue || (octets[len - 1] & 0x80) != endSign; len++)
{
octets[len] = (sbyte)(longValue & 0xFF);
longValue >>= 8;
}
Encode(n.GetIdentifier(), stream);
stream.WriteByte((byte)len);
for (var i = len - 1; i >= 0; i--)
{
stream.WriteByte((byte) octets[i]);
}
}
/// <summary>
/// Encode an Asn1OctetString directly into the specified outputstream.
/// </summary>
/// <param name="os">The Asn1OctetString object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1OctetString os, Stream stream)
{
Encode(os.GetIdentifier(), stream);
EncodeLength(os.ByteValue().Length, stream);
var tempSbyteArray = os.ByteValue();
stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length);
}
public static void Encode(Asn1Object obj, Stream stream)
{
switch (obj)
{
case Asn1Boolean b:
Encode(b, stream);
break;
case Asn1Numeric n:
Encode(n, stream);
break;
case Asn1Null n:
Encode(n.GetIdentifier(), stream);
stream.WriteByte(0x00); // Length (with no Content)
break;
case Asn1OctetString n:
Encode(n, stream);
break;
case Asn1Structured n:
Encode(n, stream);
break;
case Asn1Tagged n:
Encode(n, stream);
break;
case Asn1Choice n:
Encode(n.ChoiceValue, stream);
break;
default:
throw new InvalidDataException();
}
}
/// <summary>
/// Encode an Asn1Structured into the specified outputstream. This method
/// can be used to encode SET, SET_OF, SEQUENCE, SEQUENCE_OF.
/// </summary>
/// <param name="c">The Asn1Structured object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1Structured c, Stream stream)
{
Encode(c.GetIdentifier(), stream);
var arrayValue = c.ToArray();
using (var output = new MemoryStream())
{
foreach (var obj in arrayValue)
{
Encode(obj, output);
}
EncodeLength((int) output.Length, stream);
var tempSbyteArray = output.ToArray();
stream.Write(tempSbyteArray, 0, tempSbyteArray.Length);
}
}
/// <summary>
/// Encode an Asn1Tagged directly into the specified outputstream.
/// </summary>
/// <param name="t">The Asn1Tagged object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1Tagged t, Stream stream)
{
if (!t.Explicit)
{
Encode(t.TaggedValue, stream);
return;
}
Encode(t.GetIdentifier(), stream);
// determine the encoded length of the base type.
using (var encodedContent = new MemoryStream())
{
Encode(t.TaggedValue, encodedContent);
EncodeLength((int) encodedContent.Length, stream);
var tempSbyteArray = encodedContent.ToArray().ToSByteArray();
stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length);
}
}
/// <summary>
/// Encode an Asn1Identifier directly into the specified outputstream.
/// </summary>
/// <param name="id">The Asn1Identifier object to encode.</param>
/// <param name="stream">The stream.</param>
public static void Encode(Asn1Identifier id, Stream stream)
{
var c = (int) id.Asn1Class;
var t = id.Tag;
var ccf = (sbyte)((c << 6) | (id.Constructed ? 0x20 : 0));
if (t < 30)
{
stream.WriteByte((byte)(ccf | t));
}
else
{
stream.WriteByte((byte)(ccf | 0x1F));
EncodeTagInteger(t, stream);
}
}
/// <summary>
/// Encodes the length.
/// </summary>
/// <param name="length">The length.</param>
/// <param name="stream">The stream.</param>
private static void EncodeLength(int length, Stream stream)
{
if (length < 0x80)
{
stream.WriteByte((byte)length);
}
else
{
var octets = new sbyte[4]; // 4 bytes sufficient for 32 bit int.
sbyte n;
for (n = 0; length != 0; n++)
{
octets[n] = (sbyte)(length & 0xFF);
length >>= 8;
}
stream.WriteByte((byte)(0x80 | n));
for (var i = n - 1; i >= 0; i--)
stream.WriteByte((byte)octets[i]);
}
}
/// <summary>
/// Encodes the provided tag into the stream.
/// </summary>
/// <param name="val">The value.</param>
/// <param name="stream">The stream.</param>
private static void EncodeTagInteger(int val, Stream stream)
{
var octets = new sbyte[5];
int n;
for (n = 0; val != 0; n++)
{
octets[n] = (sbyte)(val & 0x7F);
val = val >> 7;
}
for (var i = n - 1; i > 0; i--)
{
stream.WriteByte((byte)(octets[i] | 0x80));
}
stream.WriteByte((byte)octets[0]);
}
}
}
@@ -0,0 +1,402 @@
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;
/// <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 int LdapV3 = 3;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private Connection _conn;
private bool _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 int ProtocolVersion => 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 => BindProperties == null ? null : (BindProperties.Anonymous ? null : 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 => BindProperties == null ? "simple" : 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 bool Connected => _conn?.IsConnected == true;
internal BindProperties BindProperties { get; set; }
internal List<RfcLdapMessage> Messages { get; } = new List<RfcLdapMessage>();
/// <inheritdoc />
public void Dispose()
{
if (_isDisposing) return;
_isDisposing = true;
Disconnect();
_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) => 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(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));
}
/// <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, 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
}
/// <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
_cts.Cancel();
_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)
{
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;
}
/// <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,
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);
}
/// <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 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<RfcLdapMessage>(Messages).Any(x => x.MessageId == msg.MessageId) == false)
await Task.Delay(100, ct).ConfigureAwait(false);
}
catch (ArgumentException)
{
// expected
}
var first = new List<RfcLdapMessage>(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
}
}
}
@@ -0,0 +1,292 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System;
using System.Collections.Generic;
using Exceptions;
/// <summary>
/// Encapsulates optional additional parameters or constraints to be applied to
/// an Ldap operation.
/// When included with LdapConstraints or LdapSearchConstraints
/// on an LdapConnection or with a specific operation request, it is
/// sent to the server along with operation requests.
/// </summary>
public class LdapControl
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapControl"/> class.
/// Constructs a new LdapControl object using the specified values.
/// </summary>
/// <param name="oid">The OID of the control, as a dotted string.</param>
/// <param name="critical">True if the Ldap operation should be discarded if
/// the control is not supported. False if
/// the operation can be processed without the control.</param>
/// <param name="values">The control-specific data.</param>
/// <exception cref="ArgumentException">An OID must be specified.</exception>
public LdapControl(string oid, bool critical, sbyte[] values)
{
if (oid == null)
{
throw new ArgumentException("An OID must be specified");
}
Asn1Object = new RfcControl(
oid,
new Asn1Boolean(critical),
values == null ? null : new Asn1OctetString(values));
}
/// <summary>
/// Returns the identifier of the control.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public string Id => Asn1Object.ControlType.StringValue();
/// <summary>
/// Returns whether the control is critical for the operation.
/// </summary>
/// <value>
/// <c>true</c> if critical; otherwise, <c>false</c>.
/// </value>
public bool Critical => Asn1Object.Criticality.BooleanValue();
internal static RespControlVector RegisteredControls { get; } = new RespControlVector(5);
internal RfcControl Asn1Object { get; }
/// <summary>
/// Registers a class to be instantiated on receipt of a control with the
/// given OID.
/// Any previous registration for the OID is overridden. The
/// controlClass must be an extension of LdapControl.
/// </summary>
/// <param name="oid">The object identifier of the control.</param>
/// <param name="controlClass">A class which can instantiate an LdapControl.</param>
public static void Register(string oid, Type controlClass)
=> RegisteredControls.RegisterResponseControl(oid, controlClass);
/// <summary>
/// Returns the control-specific data of the object.
/// </summary>
/// <returns>
/// The control-specific data of the object as a byte array,
/// or null if the control has no data.
/// </returns>
public sbyte[] GetValue() => Asn1Object.ControlValue?.ByteValue();
internal void SetValue(sbyte[] controlValue)
{
Asn1Object.ControlValue = new Asn1OctetString(controlValue);
}
}
/// <summary>
/// Represents a simple bind request.
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
public class LdapBindRequest : LdapMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapBindRequest"/> class.
/// Constructs a simple bind request.
/// </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.</param>
public LdapBindRequest(int version, string dn, sbyte[] password)
: base(LdapOperation.BindRequest, new RfcBindRequest(version, dn, password))
{
}
/// <summary>
/// Retrieves the Authentication DN for a bind request.
/// </summary>
/// <value>
/// The authentication dn.
/// </value>
public string AuthenticationDN => Asn1Object.RequestDn;
/// <inheritdoc />
public override string ToString() => Asn1Object.ToString();
}
/// <summary>
/// Encapsulates a continuation reference from an asynchronous search operation.
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
internal class LdapSearchResultReference : LdapMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapSearchResultReference"/> class.
/// Constructs an LdapSearchResultReference object.
/// </summary>
/// <param name="message">The LdapMessage with a search reference.</param>
internal LdapSearchResultReference(RfcLdapMessage message)
: base(message)
{
}
/// <summary>
/// Returns any URLs in the object.
/// </summary>
/// <value>
/// The referrals.
/// </value>
public string[] Referrals
{
get
{
var references = ((RfcSearchResultReference)Message.Response).ToArray();
var srefs = new string[references.Length];
for (var i = 0; i < references.Length; i++)
{
srefs[i] = ((Asn1OctetString)references[i]).StringValue();
}
return srefs;
}
}
}
internal class LdapResponse : LdapMessage
{
internal LdapResponse(RfcLdapMessage message)
: base(message)
{
}
public string ErrorMessage => ((IRfcResponse)Message.Response).GetErrorMessage().StringValue();
public string MatchedDN => ((IRfcResponse)Message.Response).GetMatchedDN().StringValue();
public LdapStatusCode ResultCode => Message.Response is RfcSearchResultEntry ||
(IRfcResponse)Message.Response is RfcIntermediateResponse
? LdapStatusCode.Success
: (LdapStatusCode)((IRfcResponse)Message.Response).GetResultCode().IntValue();
internal LdapException Exception { get; set; }
internal void ChkResultCode()
{
if (Exception != null)
{
throw Exception;
}
switch (ResultCode)
{
case LdapStatusCode.Success:
case LdapStatusCode.CompareTrue:
case LdapStatusCode.CompareFalse:
break;
case LdapStatusCode.Referral:
throw new LdapException(
"Automatic referral following not enabled",
LdapStatusCode.Referral,
ErrorMessage);
default:
throw new LdapException(ResultCode.ToString().Humanize(), ResultCode, ErrorMessage, MatchedDN);
}
}
}
/// <summary>
/// The RespControlVector class implements extends the
/// existing Vector class so that it can be used to maintain a
/// list of currently registered control responses.
/// </summary>
internal class RespControlVector : List<RespControlVector.RegisteredControl>
{
private readonly object _syncLock = new object();
public RespControlVector(int cap)
: base(cap)
{
}
public void RegisterResponseControl(string oid, Type controlClass)
{
lock (_syncLock)
{
Add(new RegisteredControl(this, oid, controlClass));
}
}
/// <summary>
/// Inner class defined to create a temporary object to encapsulate
/// all registration information about a response control.
/// </summary>
internal class RegisteredControl
{
public RegisteredControl(RespControlVector enclosingInstance, string oid, Type controlClass)
{
EnclosingInstance = enclosingInstance;
MyOid = oid;
MyClass = controlClass;
}
internal Type MyClass { get; }
internal string MyOid { get; }
private RespControlVector EnclosingInstance { get; }
}
}
/// <summary>
/// Represents and Ldap Bind Request.
/// <pre>
/// BindRequest ::= [APPLICATION 0] SEQUENCE {
/// version INTEGER (1 .. 127),
/// name LdapDN,
/// authentication AuthenticationChoice }
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="IRfcRequest" />
internal sealed class RfcBindRequest
: Asn1Sequence, IRfcRequest
{
private readonly sbyte[] _password;
private static readonly Asn1Identifier Id = new Asn1Identifier(LdapOperation.BindRequest);
public RfcBindRequest(int version, string name, sbyte[] password)
: base(3)
{
_password = password;
Add(new Asn1Integer(version));
Add(name);
Add(new RfcAuthenticationChoice(password));
}
public Asn1Integer Version
{
get => (Asn1Integer)Get(0);
set => Set(0, value);
}
public Asn1OctetString Name
{
get => (Asn1OctetString)Get(1);
set => Set(1, value);
}
public RfcAuthenticationChoice AuthenticationChoice
{
get => (RfcAuthenticationChoice)Get(2);
set => Set(2, value);
}
public override Asn1Identifier GetIdentifier() => Id;
public string GetRequestDN() => ((Asn1OctetString)Get(1)).StringValue();
}
}
+794
View File
@@ -0,0 +1,794 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// 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.
/// </summary>
/// <seealso cref="LdapAttribute"></seealso>
/// <seealso cref="LdapAttributeSet"></seealso>
public class LdapEntry
{
private readonly LdapAttributeSet _attrs;
/// <summary>
/// Initializes a new instance of the <see cref="LdapEntry" /> class.
/// Constructs a new entry with the specified distinguished name and set
/// of attributes.
/// </summary>
/// <param name="dn">The distinguished name of the new entry. The
/// value is not validated. An invalid distinguished
/// name will cause operations using this entry to fail.</param>
/// <param name="attrs">The initial set of attributes assigned to the
/// entry.</param>
public LdapEntry(string dn = null, LdapAttributeSet attrs = null)
{
DN = dn ?? string.Empty;
_attrs = attrs ?? new LdapAttributeSet();
}
/// <summary>
/// Returns the distinguished name of the entry.
/// </summary>
/// <value>
/// The dn.
/// </value>
public string DN { get; }
/// <summary>
/// Returns the attributes matching the specified attrName.
/// </summary>
/// <param name="attrName">The name of the attribute or attributes to return.</param>
/// <returns>
/// The attribute matching the name.
/// </returns>
public LdapAttribute GetAttribute(string attrName) => _attrs[attrName];
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// The attribute set of the entry.
/// </returns>
public LdapAttributeSet GetAttributeSet() => _attrs;
/// <summary>
/// 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.
/// </summary>
/// <param name="subtype">One or more subtype specification(s), separated
/// with semicolons. The "lang-ja" and
/// "lang-en;phonetic" are valid subtype
/// specifications.</param>
/// <returns>
/// An attribute set from the entry with the attributes that
/// match the specified subtypes or an empty set if no attributes
/// match.
/// </returns>
public LdapAttributeSet GetAttributeSet(string subtype) => _attrs.GetSubset(subtype);
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttribute"/> class.
/// Constructs an attribute with no values.
/// </summary>
/// <param name="attrName">Name of the attribute.</param>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
public LdapAttribute(string attrName)
{
Name = attrName ?? throw new ArgumentNullException(nameof(attrName));
_baseName = GetBaseName(attrName);
_subTypes = GetSubtypes(attrName);
}
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttribute" /> class.
/// Constructs an attribute with a single <see cref="System.String" /> value.
/// </summary>
/// <param name="attrName">Name of the attribute.</param>
/// <param name="attrString">Value of the attribute as a string.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public LdapAttribute(string attrName, string attrString)
: this(attrName)
{
Add(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Returns the values of the attribute as an array of bytes.
/// </summary>
/// <value>
/// The byte value array.
/// </value>
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;
}
}
/// <summary>
/// Returns the values of the attribute as an array of strings.
/// </summary>
/// <value>
/// The string value array.
/// </value>
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;
}
}
/// <summary>
/// Returns the the first value of the attribute as an UTF-8 string.
/// </summary>
/// <value>
/// The string value.
/// </value>
public string StringValue => _values == null ? null : Encoding.UTF8.GetString((sbyte[])_values[0]);
/// <summary>
/// Returns the first value of the attribute as a byte array or null.
/// </summary>
/// <value>
/// The byte value.
/// </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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <value>
/// The language subtype.
/// </value>
public string LangSubtype => _subTypes?.FirstOrDefault(t => t.StartsWith("lang-"));
/// <summary>
/// Returns the name of the attribute.
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name { get; }
internal string Value
{
set
{
_values = null;
Add(Encoding.UTF8.GetSBytes(value));
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="attrName">Name of the attribute from which to extract
/// the subtypes.</param>
/// <returns>
/// An array subtypes or null if the attribute has none.
/// </returns>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
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;
}
/// <summary>
/// Returns the base name of the specified attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
/// </summary>
/// <param name="attrName">Name of the attribute from which to extract the
/// base name.</param>
/// <returns> The base name of the attribute. </returns>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
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);
}
/// <summary>
/// Clones this instance.
/// </summary>
/// <returns>A cloned instance.</returns>
public LdapAttribute Clone()
{
var newObj = MemberwiseClone();
if (_values != null)
{
Array.Copy(_values, 0, ((LdapAttribute)newObj)._values, 0, _values.Length);
}
return (LdapAttribute) newObj;
}
/// <summary>
/// Adds a <see cref="System.String" /> value to the attribute.
/// </summary>
/// <param name="attrString">Value of the attribute as a String.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddValue(string attrString)
{
if (attrString == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Adds a byte-formatted value to the attribute.
/// </summary>
/// <param name="attrBytes">Value of the attribute as raw bytes.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddValue(sbyte[] attrBytes)
{
if (attrBytes == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(attrBytes);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="attrString">The base64 value of the attribute as a String.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddBase64Value(string attrString)
{
if (attrString == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(Convert.FromBase64String(attrString).ToSByteArray());
}
/// <summary>
/// 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.
/// </summary>
/// <param name="attrString">The base64 value of the attribute as a StringBuffer.</param>
/// <param name="start">The start index of base64 encoded part, inclusive.</param>
/// <param name="end">The end index of base encoded part, exclusive.</param>
/// <exception cref="ArgumentNullException">attrString.</exception>
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());
}
/// <summary>
/// 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.
/// </summary>
/// <param name="attrChars">The base64 value of the attribute as an array of
/// characters.</param>
/// <exception cref="ArgumentNullException">attrChars.</exception>
public void AddBase64Value(char[] attrChars)
{
if (attrChars == null)
{
throw new ArgumentNullException(nameof(attrChars));
}
Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray());
}
/// <summary>
/// Returns the base name of the attribute.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
/// </summary>
/// <returns>
/// The base name of the attribute.
/// </returns>
public string GetBaseName() => _baseName;
/// <summary>
/// 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.
/// </summary>
/// <returns>
/// An array subtypes or null if the attribute has none.
/// </returns>
public string[] GetSubtypes() => _subTypes;
/// <summary>
/// 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.
/// </summary>
/// <param name="subtype">
/// The single subtype to check for.
/// </param>
/// <returns>
/// True, if the attribute has the specified subtype;
/// false, if it doesn't.
/// </returns>
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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="subtypes">
/// An array of subtypes to check for.
/// </param>
/// <returns>
/// True, if the attribute has all the specified subtypes;
/// false, if it doesn't have all the subtypes.
/// </returns>
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;
}
/// <summary>
/// Removes a string value from the attribute.
/// </summary>
/// <param name="attrString">Value of the attribute as a string.
/// Note: Removing a value which is not present in the attribute has
/// no effect.</param>
/// <exception cref="ArgumentNullException">attrString.</exception>
public void RemoveValue(string attrString)
{
if (attrString == null)
{
throw new ArgumentNullException(nameof(attrString));
}
RemoveValue(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Removes a byte-formatted value from the attribute.
/// </summary>
/// <param name="attrBytes">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.
/// </param>
/// <exception cref="ArgumentNullException">attrBytes.</exception>
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;
}
}
/// <summary>
/// Returns the number of values in the attribute.
/// </summary>
/// <returns>
/// The number of values in the attribute.
/// </returns>
public int Size() => _values?.Length ?? 0;
/// <summary>
/// Compares this object with the specified object for order.
/// Ordering is determined by comparing attribute names using the method Compare() of the String class.
/// </summary>
/// <param name="attribute">The LdapAttribute to be compared to this object.</param>
/// <returns>
/// Returns a negative integer, zero, or a positive
/// integer as this object is less than, equal to, or greater than the
/// specified object.
/// </returns>
public int CompareTo(object attribute)
=> string.Compare(Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal);
/// <summary>
/// Returns a string representation of this LdapAttribute.
/// </summary>
/// <returns>
/// a string representation of this LdapAttribute.
/// </returns>
/// <exception cref="Exception">NullReferenceException.</exception>
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("<binary value, length:" + sval.Length);
continue;
}
result.Append(sval);
}
result.Append("'");
}
result.Append("}");
return result.ToString();
}
/// <summary>
/// Adds an object to this object's list of attribute values.
/// </summary>
/// <param name="bytes">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.</param>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <seealso cref="LdapAttribute"></seealso>
/// <seealso cref="LdapEntry"></seealso>
public class LdapAttributeSet : Dictionary<string, LdapAttribute>
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttributeSet"/> class.
/// </summary>
public LdapAttributeSet()
: base(StringComparer.OrdinalIgnoreCase)
{
// placeholder
}
/// <summary>
/// Creates a new attribute set containing only the attributes that have
/// the specified subtypes.
/// For example, suppose an attribute set contains the following
/// attributes:
/// <ul><li> cn</li><li> cn;lang-ja</li><li> sn;phonetic;lang-ja</li><li> sn;lang-us</li></ul>
/// Calling the <c>getSubset</c> method and passing lang-ja as the
/// argument, the method returns an attribute set containing the following
/// attributes:.
/// <ul><li>cn;lang-ja</li><li>sn;phonetic;lang-ja</li></ul>
/// </summary>
/// <param name="subtype">Semi-colon delimited list of subtypes to include. For
/// example:
/// <ul><li> "lang-ja" specifies only Japanese language subtypes</li><li> "binary" specifies only binary subtypes</li><li>
/// "binary;lang-ja" specifies only Japanese language subtypes
/// which also are binary
/// </li></ul>
/// Note: Novell eDirectory does not currently support language subtypes.
/// It does support the "binary" subtype.</param>
/// <returns>
/// An attribute set containing the attributes that match the
/// specified subtype.
/// </returns>
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;
}
/// <summary>
/// Returns <c>true</c> if this set contains an attribute of the same name
/// as the specified attribute.
/// </summary>
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
/// <returns>
/// true if this set contains the specified attribute.
/// </returns>
public bool Contains(object attr) => ContainsKey(((LdapAttribute)attr).Name);
/// <summary>
/// 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.
/// </summary>
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
/// <returns>
/// <c>true</c> if the attribute was added.
/// </returns>
public bool Add(LdapAttribute attr)
{
var name = attr.Name;
if (ContainsKey(name))
return false;
this[name] = attr;
return true;
}
/// <summary>
/// Removes the specified object from this set if it is present.
/// If the specified object is of type <c>LdapAttribute</c>, 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.
/// </summary>
/// <param name="entry">The entry.</param>
/// <returns>
/// true if the object was removed.
/// </returns>
public bool Remove(LdapAttribute entry) => Remove(entry.Name);
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
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();
}
}
}
+135
View File
@@ -0,0 +1,135 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// Ldap Modification Operators.
/// </summary>
public enum LdapModificationOp
{
/// <summary>
/// Adds the listed values to the given attribute, creating
/// the attribute if it does not already exist.
/// </summary>
Add = 0,
/// <summary>
/// Deletes the listed values from the given attribute,
/// removing the entire attribute (1) if no values are listed or
/// (2) if all current values of the attribute are listed for
/// deletion.
/// </summary>
Delete = 1,
/// <summary>
/// Replaces all existing values of the given attribute
/// with the new values listed, creating the attribute if it
/// does not already exist.
/// A replace with no value deletes the entire attribute if it
/// exists, and is ignored if the attribute does not exist.
/// </summary>
Replace = 2,
}
/// <summary>
/// LDAP valid scopes.
/// </summary>
public enum LdapScope
{
/// <summary>
/// Used with search to specify that the scope of entrys to search is to
/// search only the base object.
/// </summary>
ScopeBase = 0,
/// <summary>
/// Used with search to specify that the scope of entrys to search is to
/// search only the immediate subordinates of the base object.
/// </summary>
ScopeOne = 1,
/// <summary>
/// Used with search to specify that the scope of entrys to search is to
/// search the base object and all entries within its subtree.
/// </summary>
ScopeSub = 2,
}
/// <summary>
/// Substring Operators.
/// </summary>
internal enum SubstringOp
{
/// <summary>
/// Search Filter Identifier for an INITIAL component of a SUBSTRING.
/// Note: An initial SUBSTRING is represented as "value*".
/// </summary>
Initial = 0,
/// <summary>
/// Search Filter Identifier for an ANY component of a SUBSTRING.
/// Note: An ANY SUBSTRING is represented as "*value*".
/// </summary>
Any = 1,
/// <summary>
/// Search Filter Identifier for a FINAL component of a SUBSTRING.
/// Note: A FINAL SUBSTRING is represented as "*value".
/// </summary>
Final = 2,
}
/// <summary>
/// Filtering Operators.
/// </summary>
internal enum FilterOp
{
/// <summary>
/// Identifier for AND component.
/// </summary>
And = 0,
/// <summary>
/// Identifier for OR component.
/// </summary>
Or = 1,
/// <summary>
/// Identifier for NOT component.
/// </summary>
Not = 2,
/// <summary>
/// Identifier for EQUALITY_MATCH component.
/// </summary>
EqualityMatch = 3,
/// <summary>
/// Identifier for SUBSTRINGS component.
/// </summary>
Substrings = 4,
/// <summary>
/// Identifier for GREATER_OR_EQUAL component.
/// </summary>
GreaterOrEqual = 5,
/// <summary>
/// Identifier for LESS_OR_EQUAL component.
/// </summary>
LessOrEqual = 6,
/// <summary>
/// Identifier for PRESENT component.
/// </summary>
Present = 7,
/// <summary>
/// Identifier for APPROX_MATCH component.
/// </summary>
ApproxMatch = 8,
/// <summary>
/// Identifier for EXTENSIBLE_MATCH component.
/// </summary>
ExtensibleMatch = 9,
}
}
@@ -0,0 +1,140 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// The base class for Ldap request and response messages.
/// Subclassed by response messages used in asynchronous operations.
/// </summary>
public class LdapMessage
{
internal RfcLdapMessage Message;
private int _imsgNum = -1; // This instance LdapMessage number
private LdapOperation _messageType = LdapOperation.Unknown;
private string _stringTag;
internal LdapMessage()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LdapMessage"/> class.
/// Creates an LdapMessage when sending a protocol operation and sends
/// some optional controls with the message.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="op">The operation type of message.</param>
/// <param name="controls">The controls to use with the operation.</param>
/// <seealso cref="Type"></seealso>
internal LdapMessage(LdapOperation type, IRfcRequest op, LdapControl[] controls = null)
{
// Get a unique number for this request message
_messageType = type;
RfcControls asn1Ctrls = null;
if (controls != null)
{
// Move LdapControls into an RFC 2251 Controls object.
asn1Ctrls = new RfcControls();
foreach (var t in controls)
{
asn1Ctrls.Add(t.Asn1Object);
}
}
// create RFC 2251 LdapMessage
Message = new RfcLdapMessage(op, asn1Ctrls);
}
/// <summary>
/// Initializes a new instance of the <see cref="LdapMessage"/> class.
/// Creates an Rfc 2251 LdapMessage when the libraries receive a response
/// from a command.
/// </summary>
/// <param name="message">A response message.</param>
internal LdapMessage(RfcLdapMessage message) => Message = message;
/// <summary>
/// Returns the message ID. The message ID is an integer value
/// identifying the Ldap request and its response.
/// </summary>
/// <value>
/// The message identifier.
/// </value>
public virtual int MessageId
{
get
{
if (_imsgNum == -1)
{
_imsgNum = Message.MessageId;
}
return _imsgNum;
}
}
/// <summary>
/// Indicates whether the message is a request or a response.
/// </summary>
/// <value>
/// <c>true</c> if request; otherwise, <c>false</c>.
/// </value>
public virtual bool Request => Message.IsRequest();
internal LdapOperation Type
{
get
{
if (_messageType == LdapOperation.Unknown)
{
_messageType = Message.Type;
}
return _messageType;
}
}
internal virtual RfcLdapMessage Asn1Object => Message;
internal virtual LdapMessage RequestingMessage => Message.RequestingMessage;
/// <summary>
/// Retrieves the identifier tag for this message.
/// An identifier can be associated with a message with the
/// <c>setTag</c> method.
/// Tags are set by the application and not by the API or the server.
/// If a server response <c>isRequest() == false</c> has no tag,
/// the tag associated with the corresponding server request is used.
/// </summary>
/// <value>
/// The tag.
/// </value>
public virtual string Tag
{
get
{
if (_stringTag != null)
{
return _stringTag;
}
return Request ? null : RequestingMessage?._stringTag;
}
set => _stringTag = value;
}
private string Name => Type.ToString();
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString() => $"{Name}({MessageId}): {Message}";
}
}
@@ -0,0 +1,78 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// A single add, delete, or replace operation to an LdapAttribute.
/// An LdapModification contains information on the type of modification
/// being performed, the name of the attribute to be replaced, and the new
/// value. Multiple modifications are expressed as an array of modifications,
/// i.e., <c>LdapModification[]</c>.
/// An LdapModification or an LdapModification array enable you to modify
/// an attribute of an Ldap entry. The entire array of modifications must
/// be performed by the server as a single atomic operation in the order they
/// are listed. No changes are made to the directory unless all the operations
/// succeed. If all succeed, a success result is returned to the application.
/// It should be noted that if the connection fails during a modification,
/// it is indeterminate whether the modification occurred or not.
/// There are three types of modification operations: Add, Delete,
/// and Replace.
/// <b>Add: </b>Creates the attribute if it doesn't exist, and adds
/// the specified values. This operation must contain at least one value, and
/// all values of the attribute must be unique.
/// <b>Delete: </b>Deletes specified values from the attribute. If no
/// values are specified, or if all existing values of the attribute are
/// specified, the attribute is removed. Mandatory attributes cannot be
/// removed.
/// <b>Replace: </b>Creates the attribute if necessary, and replaces
/// all existing values of the attribute with the specified values.
/// If you wish to keep any existing values of a multi-valued attribute,
/// you must include these values in the replace operation.
/// A replace operation with no value will remove the entire attribute if it
/// exists, and is ignored if the attribute does not exist.
/// Additional information on Ldap modifications is available in section 4.6
/// of. <a href="http://www.ietf.org/rfc/rfc2251.txt">rfc2251.txt</a>
/// </summary>
/// <seealso cref="LdapConnection.Modify"></seealso>
/// <seealso cref="LdapAttribute"></seealso>
public sealed class LdapModification : LdapMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapModification" /> class.
/// Specifies a modification to be made to an attribute.
/// </summary>
/// <param name="op">The op.</param>
/// <param name="attr">The attribute to modify.</param>
public LdapModification(LdapModificationOp op, LdapAttribute attr)
{
Op = op;
Attribute = attr;
}
/// <summary>
/// Initializes a new instance of the <see cref="LdapModification"/> class.
/// </summary>
/// <param name="op">The op.</param>
/// <param name="attrName">Name of the attribute.</param>
/// <param name="attrValue">The attribute value.</param>
public LdapModification(LdapModificationOp op, string attrName, string attrValue)
: this(op, new LdapAttribute(attrName, attrValue))
{
// placeholder
}
/// <summary>
/// Returns the attribute to modify, with any existing values.
/// </summary>
/// <value>
/// The attribute.
/// </value>
public LdapAttribute Attribute { get; }
/// <summary>
/// Returns the type of modification specified by this object.
/// </summary>
/// <value>
/// The op.
/// </value>
public LdapModificationOp Op { get; }
}
}
@@ -0,0 +1,68 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// Represents a LDAP Modification Request Message.
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
public sealed class LdapModifyRequest : LdapMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapModifyRequest"/> class.
/// </summary>
/// <param name="dn">The dn.</param>
/// <param name="modifications">The modifications.</param>
/// <param name="control">The control.</param>
public LdapModifyRequest(string dn, LdapModification[] modifications, LdapControl[] control)
: base(LdapOperation.ModifyRequest, new RfcModifyRequest(dn, EncodeModifications(modifications)), control)
{
}
/// <summary>
/// Gets the dn.
/// </summary>
/// <value>
/// The dn.
/// </value>
public string DN => Asn1Object.RequestDn;
/// <inheritdoc />
public override string ToString() => Asn1Object.ToString();
private static Asn1SequenceOf EncodeModifications(LdapModification[] mods)
{
var rfcMods = new Asn1SequenceOf(mods.Length);
foreach (var t in mods)
{
var attr = t.Attribute;
var vals = new Asn1SetOf(attr.Size());
if (attr.Size() > 0)
{
foreach (var val in attr.ByteValueArray)
{
vals.Add(new Asn1OctetString(val));
}
}
var rfcMod = new Asn1Sequence(2);
rfcMod.Add(new Asn1Enumerated((int) t.Op));
rfcMod.Add(new RfcAttributeTypeAndValues(attr.Name, vals));
rfcMods.Add(rfcMod);
}
return rfcMods;
}
internal class RfcAttributeTypeAndValues : Asn1Sequence
{
public RfcAttributeTypeAndValues(string type, Asn1Object vals)
: base(2)
{
Add(type);
Add(vals);
}
}
}
}
@@ -0,0 +1,117 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// LDAP Operation.
/// </summary>
internal enum LdapOperation
{
/// <summary>
/// The unknown
/// </summary>
Unknown = -1,
/// <summary>
/// A bind request operation.
/// BIND_REQUEST = 0
/// </summary>
BindRequest = 0,
/// <summary>
/// A bind response operation.
/// BIND_RESPONSE = 1
/// </summary>
BindResponse = 1,
/// <summary>
/// An unbind request operation.
/// UNBIND_REQUEST = 2
/// </summary>
UnbindRequest = 2,
/// <summary>
/// A search request operation.
/// SEARCH_REQUEST = 3
/// </summary>
SearchRequest = 3,
/// <summary>
/// A search response containing data.
/// SEARCH_RESPONSE = 4
/// </summary>
SearchResponse = 4,
/// <summary>
/// A search result message - contains search status.
/// SEARCH_RESULT = 5
/// </summary>
SearchResult = 5,
/// <summary>
/// A modify request operation.
/// MODIFY_REQUEST = 6
/// </summary>
ModifyRequest = 6,
/// <summary>
/// A modify response operation.
/// MODIFY_RESPONSE = 7
/// </summary>
ModifyResponse = 7,
/// <summary>
/// An abandon request operation.
/// ABANDON_REQUEST = 16
/// </summary>
AbandonRequest = 16,
/// <summary>
/// A search result reference operation.
/// SEARCH_RESULT_REFERENCE = 19
/// </summary>
SearchResultReference = 19,
/// <summary>
/// An extended request operation.
/// EXTENDED_REQUEST = 23
/// </summary>
ExtendedRequest = 23,
/// <summary>
/// An extended response operation.
/// EXTENDED_RESPONSE = 24
/// </summary>
ExtendedResponse = 24,
/// <summary>
/// An intermediate response operation.
/// INTERMEDIATE_RESPONSE = 25
/// </summary>
IntermediateResponse = 25,
}
/// <summary>
/// ASN1 tags.
/// </summary>
internal enum Asn1IdentifierTag
{
/// <summary>
/// Universal tag class.
/// </summary>
Universal = 0,
/// <summary>
/// Application-wide tag class.
/// </summary>
Application = 1,
/// <summary>
/// Context-specific tag class.
/// </summary>
Context = 2,
/// <summary>
/// Private-use tag class.
/// </summary>
Private = 3,
}
}
@@ -0,0 +1,185 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.Collections;
/// <summary>
/// Represents an Ldap Search request.
/// </summary>
/// <seealso cref="LdapMessage" />
internal sealed class LdapSearchRequest : LdapMessage
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapSearchRequest"/> class.
/// </summary>
/// <param name="ldapBase">The base distinguished name to search from.</param>
/// <param name="scope">The scope of the entries to search. The following
/// are the valid options:.
/// <ul><li>SCOPE_BASE - searches only the base DN</li><li>SCOPE_ONE - searches only entries under the base DN</li><li>
/// SCOPE_SUB - searches the base DN and all entries
/// within its subtree
/// </li></ul></param>
/// <param name="filter">The search filter specifying the search criteria.</param>
/// <param name="attrs">The names of attributes to retrieve.
/// operation exceeds the time limit.</param>
/// <param name="dereference">Specifies when aliases should be dereferenced.
/// Must be one of the constants defined in
/// LdapConstraints, which are DEREF_NEVER,
/// DEREF_FINDING, DEREF_SEARCHING, or DEREF_ALWAYS.</param>
/// <param name="maxResults">The maximum number of search results to return
/// for a search request.
/// The search operation will be terminated by the server
/// with an LdapException.SIZE_LIMIT_EXCEEDED if the
/// number of results exceed the maximum.</param>
/// <param name="serverTimeLimit">The maximum time in seconds that the server
/// should spend returning search results. This is a
/// server-enforced limit. A value of 0 means
/// no time limit.</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="cont">Any controls that apply to the search request.
/// or null if none.</param>
/// <seealso cref="LdapConnection.Search"></seealso>
public LdapSearchRequest(
string ldapBase,
LdapScope scope,
string filter,
string[] attrs,
int dereference,
int maxResults,
int serverTimeLimit,
bool typesOnly,
LdapControl[] cont)
: base(
LdapOperation.SearchRequest,
new RfcSearchRequest(ldapBase, scope, dereference, maxResults, serverTimeLimit, typesOnly, filter, attrs),
cont)
{
}
/// <summary>
/// Retrieves an Iterator object representing the parsed filter for
/// this search request.
/// The first object returned from the Iterator is an Integer indicating
/// the type of filter component. One or more values follow the component
/// type as subsequent items in the Iterator. The pattern of Integer
/// component type followed by values continues until the end of the
/// filter.
/// Values returned as a byte array may represent UTF-8 characters or may
/// be binary values. The possible Integer components of a search filter
/// and the associated values that follow are:.
/// <ul><li>AND - followed by an Iterator value</li><li>OR - followed by an Iterator value</li><li>NOT - followed by an Iterator value</li><li>
/// EQUALITY_MATCH - followed by the attribute name represented as a
/// String, and by the attribute value represented as a byte array
/// </li><li>
/// GREATER_OR_EQUAL - followed by the attribute name represented as a
/// String, and by the attribute value represented as a byte array
/// </li><li>
/// LESS_OR_EQUAL - followed by the attribute name represented as a
/// String, and by the attribute value represented as a byte array
/// </li><li>
/// APPROX_MATCH - followed by the attribute name represented as a
/// String, and by the attribute value represented as a byte array
/// </li><li>PRESENT - followed by a attribute name respresented as a String</li><li>
/// EXTENSIBLE_MATCH - followed by the name of the matching rule
/// represented as a String, by the attribute name represented
/// as a String, and by the attribute value represented as a
/// byte array.
/// </li><li>
/// SUBSTRINGS - followed by the attribute name represented as a
/// String, by one or more SUBSTRING components (INITIAL, ANY,
/// or FINAL) followed by the SUBSTRING value.
/// </li></ul>
/// </summary>
/// <value>
/// The search filter.
/// </value>
public IEnumerator SearchFilter => RfcFilter.GetFilterIterator();
/// <summary>
/// Retrieves the Base DN for a search request.
/// </summary>
/// <returns>
/// the base DN for a search request.
/// </returns>
public string DN => Asn1Object.RequestDn;
/// <summary>
/// Retrieves the scope of a search request.
/// </summary>
/// <value>
/// The scope.
/// </value>
public int Scope => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(1)).IntValue();
/// <summary>
/// Retrieves the behaviour of dereferencing aliases on a search request.
/// </summary>
/// <value>
/// The dereference.
/// </value>
public int Dereference => ((Asn1Enumerated)((RfcSearchRequest)Asn1Object.Get(1)).Get(2)).IntValue();
/// <summary>
/// Retrieves the maximum number of entries to be returned on a search.
/// </summary>
/// <value>
/// The maximum results.
/// </value>
public int MaxResults => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(3)).IntValue();
/// <summary>
/// Retrieves the server time limit for a search request.
/// </summary>
/// <value>
/// The server time limit.
/// </value>
public int ServerTimeLimit => ((Asn1Integer)((RfcSearchRequest)Asn1Object.Get(1)).Get(4)).IntValue();
/// <summary>
/// Retrieves whether attribute values or only attribute types(names) should
/// be returned in a search request.
/// </summary>
/// <value>
/// <c>true</c> if [types only]; otherwise, <c>false</c>.
/// </value>
public bool TypesOnly => ((Asn1Boolean)((RfcSearchRequest)Asn1Object.Get(1)).Get(5)).BooleanValue();
/// <summary>
/// Retrieves an array of attribute names to request for in a search.
/// </summary>
/// <value>
/// The attributes.
/// </value>
public string[] Attributes
{
get
{
var attrs = (RfcAttributeDescriptionList)((RfcSearchRequest)Asn1Object.Get(1)).Get(7);
var values = new string[attrs.Size()];
for (var i = 0; i < values.Length; i++)
{
values[i] = ((Asn1OctetString)attrs.Get(i)).StringValue();
}
return values;
}
}
/// <summary>
/// Creates a string representation of the filter in this search request.
/// </summary>
/// <value>
/// The string filter.
/// </value>
public string StringFilter => RfcFilter.FilterToString();
/// <summary>
/// Retrieves an SearchFilter object representing a filter for a search request.
/// </summary>
/// <value>
/// The RFC filter.
/// </value>
private RfcFilter RfcFilter => (RfcFilter)((RfcSearchRequest)Asn1Object.Get(1)).Get(6);
}
}
@@ -0,0 +1,95 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// An LdapSearchResults object is returned from a synchronous search
/// operation. It provides access to all results received during the
/// operation (entries and exceptions).
/// </summary>
/// <seealso cref="LdapConnection.Search"></seealso>
public sealed class LdapSearchResults
{
private readonly List<RfcLdapMessage> _messages;
private readonly int _messageId;
/// <summary>
/// Initializes a new instance of the <see cref="LdapSearchResults" /> class.
/// </summary>
/// <param name="messages">The messages.</param>
/// <param name="messageId">The message identifier.</param>
internal LdapSearchResults(List<RfcLdapMessage> messages, int messageId)
{
_messages = messages;
_messageId = messageId;
}
/// <summary>
/// Returns a count of the items in the search result.
/// Returns a count of the entries and exceptions remaining in the object.
/// If the search was submitted with a batch size greater than zero,
/// getCount reports the number of results received so far but not enumerated
/// with next(). If batch size equals zero, getCount reports the number of
/// items received, since the application thread blocks until all results are
/// received.
/// </summary>
/// <value>
/// The count.
/// </value>
public int Count => new List<RfcLdapMessage>(_messages)
.Count(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult);
/// <summary>
/// Reports if there are more search results.
/// </summary>
/// <returns>
/// true if there are more search results.
/// </returns>
public bool HasMore() => new List<RfcLdapMessage>(_messages)
.Any(x => x.MessageId == _messageId && GetResponse(x) is LdapSearchResult);
/// <summary>
/// Returns the next result as an LdapEntry.
/// If automatic referral following is disabled or if a referral
/// was not followed, next() will throw an LdapReferralException
/// when the referral is received.
/// </summary>
/// <returns>
/// The next search result as an LdapEntry.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">Next - No more results.</exception>
public LdapEntry Next()
{
var list = new List<RfcLdapMessage>(_messages)
.Where(x => x.MessageId == _messageId);
foreach (var item in list)
{
_messages.Remove(item);
var response = GetResponse(item);
if (response is LdapSearchResult result)
{
return result.Entry;
}
}
throw new ArgumentOutOfRangeException(nameof(Next), "No more results");
}
private static LdapMessage GetResponse(RfcLdapMessage item)
{
switch (item.Type)
{
case LdapOperation.SearchResponse:
return new LdapSearchResult(item);
case LdapOperation.SearchResultReference:
return new LdapSearchResultReference(item);
default:
return new LdapResponse(item);
}
}
}
}
@@ -0,0 +1,539 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// LDAP Connection Status Code.
/// </summary>
public enum LdapStatusCode
{
/// <summary>
/// Indicates the requested client operation completed successfully.
/// SUCCESS = 0<p />
/// </summary>
Success = 0,
/// <summary>
/// Indicates an internal error.
/// The server is unable to respond with a more specific error and is
/// also unable to properly respond to a request. It does not indicate
/// that the client has sent an erroneous message.
/// OPERATIONS_ERROR = 1
/// </summary>
OperationsError = 1,
/// <summary>
/// Indicates that the server has received an invalid or malformed request
/// from the client.
/// PROTOCOL_ERROR = 2
/// </summary>
ProtocolError = 2,
/// <summary>
/// Indicates that the operation's time limit specified by either the
/// client or the server has been exceeded.
/// On search operations, incomplete results are returned.
/// TIME_LIMIT_EXCEEDED = 3
/// </summary>
TimeLimitExceeded = 3,
/// <summary>
/// Indicates that in a search operation, the size limit specified by
/// the client or the server has been exceeded. Incomplete results are
/// returned.
/// SIZE_LIMIT_EXCEEDED = 4
/// </summary>
SizeLimitExceeded = 4,
/// <summary>
/// Does not indicate an error condition. Indicates that the results of
/// a compare operation are false.
/// COMPARE_FALSE = 5
/// </summary>
CompareFalse = 5,
/// <summary>
/// Does not indicate an error condition. Indicates that the results of a
/// compare operation are true.
/// COMPARE_TRUE = 6
/// </summary>
CompareTrue = 6,
/// <summary>
/// Indicates that during a bind operation the client requested an
/// authentication method not supported by the Ldap server.
/// AUTH_METHOD_NOT_SUPPORTED = 7
/// </summary>
AuthMethodNotSupported = 7,
/// <summary>
/// Indicates a problem with the level of authentication.
/// One of the following has occurred:
/// <ul><li>
/// In bind requests, the Ldap server accepts only strong
/// authentication.
/// </li><li>
/// In a client request, the client requested an operation such as delete
/// that requires strong authentication.
/// </li><li>
/// In an unsolicited notice of disconnection, the Ldap server discovers
/// the security protecting the communication between the client and
/// server has unexpectedly failed or been compromised.
/// </li></ul>
/// STRONG_AUTH_REQUIRED = 8
/// </summary>
StrongAuthRequired = 8,
/// <summary>
/// Returned by some Ldap servers to Ldapv2 clients to indicate that a referral
/// has been returned in the error string.
/// Ldap_PARTIAL_RESULTS = 9
/// </summary>
LdapPartialResults = 9,
/// <summary>
/// Does not indicate an error condition. In Ldapv3, indicates that the server
/// does not hold the target entry of the request, but that the servers in the
/// referral field may.
/// REFERRAL = 10
/// </summary>
Referral = 10,
/// <summary>
/// Indicates that an Ldap server limit set by an administrative authority
/// has been exceeded.
/// ADMIN_LIMIT_EXCEEDED = 11
/// </summary>
AdminLimitExceeded = 11,
/// <summary>
/// Indicates that the Ldap server was unable to satisfy a request because
/// one or more critical extensions were not available.
/// Either the server does not support the control or the control is not
/// appropriate for the operation type.
/// UNAVAILABLE_CRITICAL_EXTENSION = 12
/// </summary>
UnavailableCriticalExtension = 12,
/// <summary>
/// Indicates that the session is not protected by a protocol such as
/// Transport Layer Security (TLS), which provides session confidentiality.
/// CONFIDENTIALITY_REQUIRED = 13
/// </summary>
ConfidentialityRequired = 13,
/// <summary>
/// Does not indicate an error condition, but indicates that the server is
/// ready for the next step in the process. The client must send the server
/// the same SASL mechanism to continue the process.
/// SASL_BIND_IN_PROGRESS = 14
/// </summary>
SaslBindInProgress = 14,
/// <summary>
/// Indicates that the attribute specified in the modify or compare
/// operation does not exist in the entry.
/// NO_SUCH_ATTRIBUTE = 16
/// </summary>
NoSuchAttribute = 16,
/// <summary>
/// Indicates that the attribute specified in the modify or add operation
/// does not exist in the Ldap server's schema.
/// UNDEFINED_ATTRIBUTE_TYPE = 17
/// </summary>
UndefinedAttributeType = 17,
/// <summary>
/// Indicates that the matching rule specified in the search filter does
/// not match a rule defined for the attribute's syntax.
/// INAPPROPRIATE_MATCHING = 18
/// </summary>
InappropriateMatching = 18,
/// <summary>
/// Indicates that the attribute value specified in a modify, add, or
/// modify DN operation violates constraints placed on the attribute. The
/// constraint can be one of size or content (for example, string only,
/// no binary data).
/// CONSTRAINT_VIOLATION = 19
/// </summary>
ConstraintViolation = 19,
/// <summary>
/// Indicates that the attribute value specified in a modify or add
/// operation already exists as a value for that attribute.
/// ATTRIBUTE_OR_VALUE_EXISTS = 20
/// </summary>
AttributeOrValueExists = 20,
/// <summary>
/// Indicates that the attribute value specified in an add, compare, or
/// modify operation is an unrecognized or invalid syntax for the attribute.
/// INVALID_ATTRIBUTE_SYNTAX = 21
/// </summary>
InvalidAttributeSyntax = 21,
/// <summary>
/// Indicates the target object cannot be found.
/// This code is not returned on the following operations:
/// <ul>
/// <li>
/// Search operations that find the search base but cannot find any
/// entries that match the search filter.
/// </li>
/// <li>Bind operations.</li>
/// </ul>
/// NO_SUCH_OBJECT = 32
/// </summary>
NoSuchObject = 32,
/// <summary>
/// Indicates that an error occurred when an alias was dereferenced.
/// ALIAS_PROBLEM = 33
/// </summary>
AliasProblem = 33,
/// <summary>
/// Indicates that the syntax of the DN is incorrect.
/// If the DN syntax is correct, but the Ldap server's structure
/// rules do not permit the operation, the server returns
/// Ldap_UNWILLING_TO_PERFORM.
/// INVALID_DN_SYNTAX = 34
/// </summary>
InvalidDnSyntax = 34,
/// <summary>
/// Indicates that the specified operation cannot be performed on a
/// leaf entry.
/// This code is not currently in the Ldap specifications, but is
/// reserved for this constant.
/// IS_LEAF = 35
/// </summary>
IsLeaf = 35,
/// <summary>
/// Indicates that during a search operation, either the client does not
/// have access rights to read the aliased object's name or dereferencing
/// is not allowed.
/// ALIAS_DEREFERENCING_PROBLEM = 36
/// </summary>
AliasDereferencingProblem = 36,
/// <summary>
/// Indicates that during a bind operation, the client is attempting to use
/// an authentication method that the client cannot use correctly.
/// For example, either of the following cause this error:
/// <ul>
/// <li>
/// The client returns simple credentials when strong credentials are
/// required.
/// </li>
/// <li>
/// The client returns a DN and a password for a simple bind when the
/// entry does not have a password defined.
/// </li>
/// </ul>
/// INAPPROPRIATE_AUTHENTICATION = 48
/// </summary>
InappropriateAuthentication = 48,
/// <summary>
/// Indicates that invalid information was passed during a bind operation.
/// One of the following occurred:
/// <ul>
/// <li> The client passed either an incorrect DN or password.</li>
/// <li>
/// The password is incorrect because it has expired, intruder detection
/// has locked the account, or some other similar reason.
/// </li>
/// </ul>
/// INVALID_CREDENTIALS = 49
/// </summary>
InvalidCredentials = 49,
/// <summary>
/// Indicates that the caller does not have sufficient rights to perform
/// the requested operation.
/// INSUFFICIENT_ACCESS_RIGHTS = 50
/// </summary>
InsufficientAccessRights = 50,
/// <summary>
/// Indicates that the Ldap server is too busy to process the client request
/// at this time, but if the client waits and resubmits the request, the
/// server may be able to process it then.
/// BUSY = 51
/// </summary>
Busy = 51,
/// <summary>
/// Indicates that the Ldap server cannot process the client's bind
/// request, usually because it is shutting down.
/// UNAVAILABLE = 52
/// </summary>
Unavailable = 52,
/// <summary>
/// Indicates that the Ldap server cannot process the request because of
/// server-defined restrictions.
/// This error is returned for the following reasons:
/// <ul>
/// <li>The add entry request violates the server's structure rules.</li>
/// <li>
/// The modify attribute request specifies attributes that users
/// cannot modify.
/// </li>
/// </ul>
/// UNWILLING_TO_PERFORM = 53
/// </summary>
UnwillingToPerform = 53,
/// <summary>
/// Indicates that the client discovered an alias or referral loop,
/// and is thus unable to complete this request.
/// LOOP_DETECT = 54
/// </summary>
LoopDetect = 54,
/// <summary>
/// Indicates that the add or modify DN operation violates the schema's
/// structure rules.
/// For example,
/// <ul>
/// <li>The request places the entry subordinate to an alias.</li>
/// <li>
/// The request places the entry subordinate to a container that
/// is forbidden by the containment rules.
/// </li>
/// <li>The RDN for the entry uses a forbidden attribute type.</li>
/// </ul>
/// NAMING_VIOLATION = 64
/// </summary>
NamingViolation = 64,
/// <summary>
/// Indicates that the add, modify, or modify DN operation violates the
/// object class rules for the entry.
/// For example, the following types of request return this error:
/// <ul>
/// <li>
/// The add or modify operation tries to add an entry without a value
/// for a required attribute.
/// </li>
/// <li>
/// The add or modify operation tries to add an entry with a value for
/// an attribute which the class definition does not contain.
/// </li>
/// <li>
/// The modify operation tries to remove a required attribute without
/// removing the auxiliary class that defines the attribute as required.
/// </li>
/// </ul>
/// OBJECT_CLASS_VIOLATION = 65
/// </summary>
ObjectClassViolation = 65,
/// <summary>
/// Indicates that the requested operation is permitted only on leaf entries.
/// For example, the following types of requests return this error:
/// <ul>
/// <li>The client requests a delete operation on a parent entry.</li>
/// <li> The client request a modify DN operation on a parent entry.</li>
/// </ul>
/// NOT_ALLOWED_ON_NONLEAF = 66
/// </summary>
NotAllowedOnNonleaf = 66,
/// <summary>
/// Indicates that the modify operation attempted to remove an attribute
/// value that forms the entry's relative distinguished name.
/// NOT_ALLOWED_ON_RDN = 67
/// </summary>
NotAllowedOnRdn = 67,
/// <summary>
/// Indicates that the add operation attempted to add an entry that already
/// exists, or that the modify operation attempted to rename an entry to the
/// name of an entry that already exists.
/// ENTRY_ALREADY_EXISTS = 68
/// </summary>
EntryAlreadyExists = 68,
/// <summary>
/// Indicates that the modify operation attempted to modify the structure
/// rules of an object class.
/// OBJECT_CLASS_MODS_PROHIBITED = 69
/// </summary>
ObjectClassModsProhibited = 69,
/// <summary>
/// Indicates that the modify DN operation moves the entry from one Ldap
/// server to another and thus requires more than one Ldap server.
/// AFFECTS_MULTIPLE_DSAS = 71
/// </summary>
AffectsMultipleDsas = 71,
/// <summary>
/// Indicates an unknown error condition.
/// OTHER = 80
/// </summary>
Other = 80,
/////////////////////////////////////////////////////////////////////////////
// Local Errors, resulting from actions other than an operation on a server
/////////////////////////////////////////////////////////////////////////////
/// <summary>
/// Indicates that the Ldap libraries cannot establish an initial connection
/// with the Ldap server. Either the Ldap server is down or the specified
/// host name or port number is incorrect.
/// SERVER_DOWN = 81
/// </summary>
ServerDown = 81,
/// <summary>
/// Indicates that the Ldap client has an error. This is usually a failed
/// dynamic memory allocation error.
/// LOCAL_ERROR = 82
/// </summary>
LocalError = 82,
/// <summary>
/// Indicates that the Ldap client encountered errors when encoding an
/// Ldap request intended for the Ldap server.
/// ENCODING_ERROR = 83
/// </summary>
EncodingError = 83,
/// <summary>
/// Indicates that the Ldap client encountered errors when decoding an
/// Ldap response from the Ldap server.
/// DECODING_ERROR = 84
/// </summary>
DecodingError = 84,
/// <summary>
/// Indicates that the time limit of the Ldap client was exceeded while
/// waiting for a result.
/// Ldap_TIMEOUT = 85
/// </summary>
LdapTimeout = 85,
/// <summary>
/// Indicates that a bind method was called with an unknown
/// authentication method.
/// AUTH_UNKNOWN = 86
/// </summary>
AuthUnknown = 86,
/// <summary>
/// Indicates that the search method was called with an invalid
/// search filter.
/// FILTER_ERROR = 87
/// </summary>
FilterError = 87,
/// <summary>
/// Indicates that the user cancelled the Ldap operation.
/// USER_CANCELLED = 88
/// </summary>
UserCancelled = 88,
/// <summary>
/// Indicates that a dynamic memory allocation method failed when calling
/// an Ldap method.
/// NO_MEMORY = 90
/// </summary>
NoMemory = 90,
/// <summary>
/// Indicates that the Ldap client has lost either its connection or
/// cannot establish a connection to the Ldap server.
/// CONNECT_ERROR = 91
/// </summary>
ConnectError = 91,
/// <summary>
/// Indicates that the requested functionality is not supported by the
/// client. For example, if the Ldap client is established as an Ldapv2
/// client, the libraries set this error code when the client requests
/// Ldapv3 functionality.
/// Ldap_NOT_SUPPORTED = 92
/// </summary>
LdapNotSupported = 92,
/// <summary>
/// Indicates that the client requested a control that the libraries
/// cannot find in the list of supported controls sent by the Ldap server.
/// CONTROL_NOT_FOUND = 93
/// </summary>
ControlNotFound = 93,
/// <summary>
/// Indicates that the Ldap server sent no results.
/// NO_RESULTS_RETURNED = 94
/// </summary>
NoResultsReturned = 94,
/// <summary>
/// Indicates that more results are chained in the result message.
/// MORE_RESULTS_TO_RETURN = 95
/// </summary>
MoreResultsToReturn = 95,
/// <summary>
/// Indicates the Ldap libraries detected a loop. Usually this happens
/// when following referrals.
/// CLIENT_LOOP = 96
/// </summary>
ClientLoop = 96,
/// <summary>
/// Indicates that the referral exceeds the hop limit. The default hop
/// limit is ten.
/// The hop limit determines how many servers the client can hop through
/// to retrieve data. For example, suppose the following conditions:
/// <ul>
/// <li>Suppose the hop limit is two.</li>
/// <li>
/// If the referral is to server D which can be contacted only through
/// server B (1 hop) which contacts server C (2 hops) which contacts
/// server D (3 hops).
/// </li>
/// </ul>
/// With these conditions, the hop limit is exceeded and the Ldap
/// libraries set this code.
/// REFERRAL_LIMIT_EXCEEDED = 97
/// </summary>
ReferralLimitExceeded = 97,
/// <summary>
/// Indicates that the server response to a request is invalid.
/// INVALID_RESPONSE = 100
/// </summary>
InvalidResponse = 100,
/// <summary>
/// Indicates that the server response to a request is ambiguous.
/// AMBIGUOUS_RESPONSE = 101
/// </summary>
AmbiguousResponse = 101,
/// <summary>
/// Indicates that TLS is not supported on the server.
/// TLS_NOT_SUPPORTED = 112
/// </summary>
TlsNotSupported = 112,
/// <summary>
/// Indicates that SSL Handshake could not succeed.
/// SSL_HANDSHAKE_FAILED = 113
/// </summary>
SslHandshakeFailed = 113,
/// <summary>
/// Indicates that SSL Provider could not be found.
/// SSL_PROVIDER_NOT_FOUND = 114
/// </summary>
SslProviderNotFound = 114,
}
}
+304
View File
@@ -0,0 +1,304 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System;
using System.Collections.Generic;
/// <summary>
/// The class performs token processing from strings.
/// </summary>
internal class Tokenizer
{
// The tokenizer uses the default delimiter set: the space character, the tab character, the newline character, and the carriage-return character
private readonly string _delimiters = " \t\n\r";
private readonly bool _returnDelims;
private List<string> _elements;
private string _source;
/// <summary>
/// Initializes a new instance of the <see cref="Tokenizer" /> class.
/// Initializes a new class instance with a specified string to process
/// and the specified token delimiters to use.
/// </summary>
/// <param name="source">String to tokenize.</param>
/// <param name="delimiters">String containing the delimiters.</param>
/// <param name="retDel">if set to <c>true</c> [ret delete].</param>
public Tokenizer(string source, string delimiters, bool retDel = false)
{
_elements = new List<string>();
_delimiters = delimiters ?? _delimiters;
_source = source;
_returnDelims = retDel;
if (_returnDelims)
Tokenize();
else
_elements.AddRange(source.Split(_delimiters.ToCharArray()));
RemoveEmptyStrings();
}
public int Count => _elements.Count;
public bool HasMoreTokens() => _elements.Count > 0;
public string NextToken()
{
if (_source == string.Empty) throw new InvalidOperationException();
string result;
if (_returnDelims)
{
RemoveEmptyStrings();
result = _elements[0];
_elements.RemoveAt(0);
return result;
}
_elements = new List<string>();
_elements.AddRange(_source.Split(_delimiters.ToCharArray()));
RemoveEmptyStrings();
result = _elements[0];
_elements.RemoveAt(0);
_source = _source.Remove(_source.IndexOf(result, StringComparison.Ordinal), result.Length);
_source = _source.TrimStart(_delimiters.ToCharArray());
return result;
}
private void RemoveEmptyStrings()
{
for (var index = 0; index < _elements.Count; index++)
{
if (_elements[index] != string.Empty) continue;
_elements.RemoveAt(index);
index--;
}
}
private void Tokenize()
{
var tempstr = _source;
if (tempstr.IndexOfAny(_delimiters.ToCharArray()) < 0 && tempstr.Length > 0)
{
_elements.Add(tempstr);
}
else if (tempstr.IndexOfAny(_delimiters.ToCharArray()) < 0 && tempstr.Length <= 0)
{
return;
}
while (tempstr.IndexOfAny(_delimiters.ToCharArray()) >= 0)
{
if (tempstr.IndexOfAny(_delimiters.ToCharArray()) == 0)
{
if (tempstr.Length > 1)
{
_elements.Add(tempstr.Substring(0, 1));
tempstr = tempstr.Substring(1);
}
else
{
tempstr = string.Empty;
}
}
else
{
var toks = tempstr.Substring(0, tempstr.IndexOfAny(_delimiters.ToCharArray()));
_elements.Add(toks);
_elements.Add(tempstr.Substring(toks.Length, 1));
tempstr = tempstr.Length > toks.Length + 1 ? tempstr.Substring(toks.Length + 1) : string.Empty;
}
}
if (tempstr.Length > 0)
{
_elements.Add(tempstr);
}
}
}
/// <summary>
/// Represents an Ldap Matching Rule Assertion.
/// <pre>
/// MatchingRuleAssertion ::= SEQUENCE {
/// matchingRule [1] MatchingRuleId OPTIONAL,
/// type [2] AttributeDescription OPTIONAL,
/// matchValue [3] AssertionValue,
/// dnAttributes [4] BOOLEAN DEFAULT FALSE }
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal class RfcMatchingRuleAssertion : Asn1Sequence
{
public RfcMatchingRuleAssertion(
string matchingRule,
string type,
sbyte[] matchValue,
Asn1Boolean dnAttributes = null)
: base(4)
{
if (matchingRule != null)
Add(new Asn1Tagged(new Asn1Identifier(1), new Asn1OctetString(matchingRule), false));
if (type != null)
Add(new Asn1Tagged(new Asn1Identifier(2), new Asn1OctetString(type), false));
Add(new Asn1Tagged(new Asn1Identifier(3), new Asn1OctetString(matchValue), false));
// if dnAttributes if false, that is the default value and we must not
// encode it. (See RFC 2251 5.1 number 4)
if (dnAttributes != null && dnAttributes.BooleanValue())
Add(new Asn1Tagged(new Asn1Identifier(4), dnAttributes, false));
}
}
/// <summary>
/// The AttributeDescriptionList is used to list attributes to be returned in
/// a search request.
/// <pre>
/// AttributeDescriptionList ::= SEQUENCE OF
/// AttributeDescription
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
internal class RfcAttributeDescriptionList : Asn1SequenceOf
{
public RfcAttributeDescriptionList(string[] attrs)
: base(attrs?.Length ?? 0)
{
if (attrs == null) return;
foreach (var attr in attrs)
{
Add(attr);
}
}
}
/// <summary>
/// Represents an Ldap Search Request.
/// <pre>
/// SearchRequest ::= [APPLICATION 3] SEQUENCE {
/// baseObject LdapDN,
/// scope ENUMERATED {
/// baseObject (0),
/// singleLevel (1),
/// wholeSubtree (2) },
/// derefAliases ENUMERATED {
/// neverDerefAliases (0),
/// derefInSearching (1),
/// derefFindingBaseObj (2),
/// derefAlways (3) },
/// sizeLimit INTEGER (0 .. maxInt),
/// timeLimit INTEGER (0 .. maxInt),
/// typesOnly BOOLEAN,
/// filter Filter,
/// attributes AttributeDescriptionList }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcRequest" />
internal class RfcSearchRequest : Asn1Sequence, IRfcRequest
{
public RfcSearchRequest(
string basePath,
LdapScope scope,
int derefAliases,
int sizeLimit,
int timeLimit,
bool typesOnly,
string filter,
string[] attributes)
: base(8)
{
Add(basePath);
Add(new Asn1Enumerated(scope));
Add(new Asn1Enumerated(derefAliases));
Add(new Asn1Integer(sizeLimit));
Add(new Asn1Integer(timeLimit));
Add(new Asn1Boolean(typesOnly));
Add(new RfcFilter(filter));
Add(new RfcAttributeDescriptionList(attributes));
}
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchRequest);
public string GetRequestDN() => ((Asn1OctetString) Get(0)).StringValue();
}
/// <summary>
/// Represents an Ldap Substring Filter.
/// <pre>
/// SubstringFilter ::= SEQUENCE {
/// type AttributeDescription,
/// -- at least one must be present
/// substrings SEQUENCE OF CHOICE {
/// initial [0] LdapString,
/// any [1] LdapString,
/// final [2] LdapString } }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal class RfcSubstringFilter : Asn1Sequence
{
public RfcSubstringFilter(string type, Asn1Object substrings)
: base(2)
{
Add(type);
Add(substrings);
}
}
/// <summary>
/// Represents an Ldap Attribute Value Assertion.
/// <pre>
/// AttributeValueAssertion ::= SEQUENCE {
/// attributeDesc AttributeDescription,
/// assertionValue AssertionValue }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal class RfcAttributeValueAssertion : Asn1Sequence
{
public RfcAttributeValueAssertion(string ad, sbyte[] av)
: base(2)
{
Add(ad);
Add(new Asn1OctetString(av));
}
public string AttributeDescription => ((Asn1OctetString) Get(0)).StringValue();
public sbyte[] AssertionValue => ((Asn1OctetString) Get(1)).ByteValue();
}
/// <summary> Encapsulates an Ldap Bind properties.</summary>
internal class BindProperties
{
/// <summary>
/// Initializes a new instance of the <see cref="BindProperties" /> class.
/// </summary>
/// <param name="version">The version.</param>
/// <param name="dn">The dn.</param>
/// <param name="method">The method.</param>
/// <param name="anonymous">if set to <c>true</c> [anonymous].</param>
public BindProperties(
int version,
string dn,
string method,
bool anonymous)
{
ProtocolVersion = version;
AuthenticationDN = dn;
AuthenticationMethod = method;
Anonymous = anonymous;
}
public int ProtocolVersion { get; }
public string AuthenticationDN { get; }
public string AuthenticationMethod { get; }
public bool Anonymous { get; }
}
}
@@ -0,0 +1,131 @@
namespace Unosquare.Swan.Networking.Ldap
{
/// <summary>
/// Represents an Ldap Control.
/// <pre>
/// Control ::= SEQUENCE {
/// controlType LdapOID,
/// criticality BOOLEAN DEFAULT FALSE,
/// controlValue OCTET STRING OPTIONAL }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal class RfcControl : Asn1Sequence
{
/// <summary>
/// Initializes a new instance of the <see cref="RfcControl"/> class.
/// Note: criticality is only added if true, as per RFC 2251 sec 5.1 part
/// (4): If a value of a type is its default value, it MUST be
/// absent.
/// </summary>
/// <param name="controlType">Type of the control.</param>
/// <param name="criticality">The criticality.</param>
/// <param name="controlValue">The control value.</param>
public RfcControl(string controlType, Asn1Boolean criticality = null, Asn1Object controlValue = null)
: base(3)
{
Add(controlType);
Add(criticality ?? new Asn1Boolean(false));
if (controlValue != null)
Add(controlValue);
}
public RfcControl(Asn1Structured seqObj)
: base(3)
{
for (var i = 0; i < seqObj.Size(); i++)
Add(seqObj.Get(i));
}
public Asn1OctetString ControlType => (Asn1OctetString)Get(0);
public Asn1Boolean Criticality => Size() > 1 && Get(1) is Asn1Boolean boolean ? boolean : new Asn1Boolean(false);
public Asn1OctetString ControlValue
{
get
{
if (Size() > 2)
{
// MUST be a control value
return (Asn1OctetString)Get(2);
}
return Size() > 1 && Get(1) is Asn1OctetString s ? s : null;
}
set
{
if (value == null)
return;
if (Size() == 3)
{
// We already have a control value, replace it
Set(2, value);
return;
}
if (Size() == 2)
{
// Get the second element
var obj = Get(1);
// Is this a control value
if (obj is Asn1OctetString)
{
// replace this one
Set(1, value);
}
else
{
// add a new one at the end
Add(value);
}
}
}
}
}
/// <summary>
/// Represents Ldap Sasl Credentials.
/// <pre>
/// SaslCredentials ::= SEQUENCE {
/// mechanism LdapString,
/// credentials OCTET STRING OPTIONAL }
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal class RfcSaslCredentials : Asn1Sequence
{
public RfcSaslCredentials(string mechanism, sbyte[] credentials = null)
: base(2)
{
Add(mechanism);
if (credentials != null)
Add(new Asn1OctetString(credentials));
}
}
/// <summary>
/// Represents an Ldap Authentication Choice.
/// <pre>
/// AuthenticationChoice ::= CHOICE {
/// simple [0] OCTET STRING,
/// -- 1 and 2 reserved
/// sasl [3] SaslCredentials }
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Choice" />
internal class RfcAuthenticationChoice : Asn1Choice
{
public RfcAuthenticationChoice(sbyte[] passwd)
: base(new Asn1Tagged(new Asn1Identifier(0), new Asn1OctetString(passwd), false))
{
}
public RfcAuthenticationChoice(string mechanism, sbyte[] credentials)
: base(new Asn1Tagged(new Asn1Identifier(3, true), new RfcSaslCredentials(mechanism, credentials), false))
{
// implicit tagging
}
}
}
File diff suppressed because it is too large Load Diff
+246
View File
@@ -0,0 +1,246 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.IO;
/// <summary>
/// Encapsulates a single search result that is in response to an asynchronous
/// search operation.
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.LdapMessage" />
internal class LdapSearchResult : LdapMessage
{
private LdapEntry _entry;
internal LdapSearchResult(RfcLdapMessage message)
: base(message)
{
}
public LdapEntry Entry
{
get
{
if (_entry != null) return _entry;
var attrs = new LdapAttributeSet();
var entry = (RfcSearchResultEntry) Message.Response;
foreach (var o in entry.Attributes.ToArray())
{
var seq = (Asn1Sequence) o;
var attr = new LdapAttribute(((Asn1OctetString)seq.Get(0)).StringValue());
var set = (Asn1Set)seq.Get(1);
foreach (var t in set.ToArray())
{
attr.AddValue(((Asn1OctetString)t).ByteValue());
}
attrs.Add(attr);
}
_entry = new LdapEntry(entry.ObjectName, attrs);
return _entry;
}
}
public override string ToString() => _entry?.ToString() ?? base.ToString();
}
/// <summary>
/// Represents an Ldap Search Result Reference.
/// <pre>
/// SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LdapURL
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
internal class RfcSearchResultReference : Asn1SequenceOf
{
/// <summary>
/// Initializes a new instance of the <see cref="RfcSearchResultReference"/> class.
/// The only time a client will create a SearchResultReference is when it is
/// decoding it from an Stream.
/// </summary>
/// <param name="stream">The streab.</param>
/// <param name="len">The length.</param>
public RfcSearchResultReference(Stream stream, int len)
: base(stream, len)
{
}
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResultReference);
}
/// <summary>
/// Represents an Ldap Extended Response.
/// <pre>
/// ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
/// COMPONENTS OF LdapResult,
/// responseName [10] LdapOID OPTIONAL,
/// response [11] OCTET STRING OPTIONAL }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
internal class RfcExtendedResponse : Asn1Sequence, IRfcResponse
{
public const int ResponseNameCode = 10;
public const int ResponseCode = 11;
private readonly int _referralIndex;
private readonly int _responseNameIndex;
private readonly int _responseIndex;
/// <summary>
/// Initializes a new instance of the <see cref="RfcExtendedResponse"/> class.
/// The only time a client will create a ExtendedResponse is when it is
/// decoding it from an stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">The length.</param>
public RfcExtendedResponse(Stream stream, int len)
: base(stream, len)
{
if (Size() <= 3) return;
for (var i = 3; i < Size(); i++)
{
var obj = (Asn1Tagged) Get(i);
var id = obj.GetIdentifier();
switch (id.Tag)
{
case RfcLdapResult.Referral:
var content = ((Asn1OctetString) obj.TaggedValue).ByteValue();
using (var bais = new MemoryStream(content.ToByteArray()))
Set(i, new Asn1SequenceOf(bais, content.Length));
_referralIndex = i;
break;
case ResponseNameCode:
Set(i, new Asn1OctetString(((Asn1OctetString) obj.TaggedValue).ByteValue()));
_responseNameIndex = i;
break;
case ResponseCode:
Set(i, obj.TaggedValue);
_responseIndex = i;
break;
}
}
}
public Asn1OctetString ResponseName => _responseNameIndex != 0 ? (Asn1OctetString) Get(_responseNameIndex) : null;
public Asn1OctetString Response => _responseIndex != 0 ? (Asn1OctetString) Get(_responseIndex) : null;
public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0);
public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue());
public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue());
public Asn1SequenceOf GetReferral()
=> _referralIndex != 0 ? (Asn1SequenceOf) Get(_referralIndex) : null;
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ExtendedResponse);
}
/// <summary>
/// Represents and Ldap Bind Response.
/// <pre>
/// BindResponse ::= [APPLICATION 1] SEQUENCE {
/// COMPONENTS OF LdapResult,
/// serverSaslCreds [7] OCTET STRING OPTIONAL }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
internal class RfcBindResponse : Asn1Sequence, IRfcResponse
{
/// <summary>
/// Initializes a new instance of the <see cref="RfcBindResponse"/> class.
/// The only time a client will create a BindResponse is when it is
/// decoding it from an InputStream
/// Note: If serverSaslCreds is included in the BindResponse, it does not
/// need to be decoded since it is already an OCTET STRING.
/// </summary>
/// <param name="stream">The in renamed.</param>
/// <param name="len">The length.</param>
public RfcBindResponse(Stream stream, int len)
: base(stream, len)
{
// Decode optional referral from Asn1OctetString to Referral.
if (Size() <= 3) return;
var obj = (Asn1Tagged) Get(3);
if (obj.GetIdentifier().Tag != RfcLdapResult.Referral) return;
var content = ((Asn1OctetString) obj.TaggedValue).ByteValue();
using (var bais = new MemoryStream(content.ToByteArray()))
Set(3, new Asn1SequenceOf(bais, content.Length));
}
public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0);
public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue());
public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue());
public Asn1SequenceOf GetReferral() => Size() > 3 && Get(3) is Asn1SequenceOf ? (Asn1SequenceOf) Get(3) : null;
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.BindResponse);
}
/// <summary>
/// Represents an LDAP Intermediate Response.
/// IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
/// COMPONENTS OF LDAPResult, note: only present on incorrectly
/// encoded response from pre Falcon-sp1 server
/// responseName [10] LDAPOID OPTIONAL,
/// responseValue [11] OCTET STRING OPTIONAL }.
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
internal class RfcIntermediateResponse : Asn1Sequence, IRfcResponse
{
public const int TagResponseName = 0;
public const int TagResponse = 1;
public RfcIntermediateResponse(Stream stream, int len)
: base(stream, len)
{
var i = Size() >= 3 ? 3 : 0;
for (; i < Size(); i++)
{
var obj = (Asn1Tagged) Get(i);
switch (obj.GetIdentifier().Tag)
{
case TagResponseName:
Set(i, new Asn1OctetString(((Asn1OctetString) obj.TaggedValue).ByteValue()));
break;
case TagResponse:
Set(i, obj.TaggedValue);
break;
}
}
}
public Asn1Enumerated GetResultCode() => Size() > 3 ? (Asn1Enumerated) Get(0) : null;
public Asn1OctetString GetMatchedDN() => Size() > 3 ? new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue()) : null;
public Asn1OctetString GetErrorMessage() =>
Size() > 3 ? new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue()) : null;
public Asn1SequenceOf GetReferral() => Size() > 3 ? (Asn1SequenceOf) Get(3) : null;
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.IntermediateResponse);
}
}
@@ -0,0 +1,390 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System;
using System.IO;
/// <summary>
/// Represents an Ldap Message.
/// <pre>
/// LdapMessage ::= SEQUENCE {
/// messageID MessageID,
/// protocolOp CHOICE {
/// bindRequest BindRequest,
/// bindResponse BindResponse,
/// unbindRequest UnbindRequest,
/// searchRequest SearchRequest,
/// searchResEntry SearchResultEntry,
/// searchResDone SearchResultDone,
/// searchResRef SearchResultReference,
/// modifyRequest ModifyRequest,
/// modifyResponse ModifyResponse,
/// addRequest AddRequest,
/// addResponse AddResponse,
/// delRequest DelRequest,
/// delResponse DelResponse,
/// modDNRequest ModifyDNRequest,
/// modDNResponse ModifyDNResponse,
/// compareRequest CompareRequest,
/// compareResponse CompareResponse,
/// abandonRequest AbandonRequest,
/// extendedReq ExtendedRequest,
/// extendedResp ExtendedResponse },
/// controls [0] Controls OPTIONAL }
/// </pre>
/// Note: The creation of a MessageID should be hidden within the creation of
/// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
/// upper and lower limit. There is never a case when a user should be
/// able to specify the MessageID for an RfcLdapMessage. The MessageID()
/// constructor should be package protected. (So the MessageID value
/// isn't arbitrarily run up.).
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal sealed class RfcLdapMessage : Asn1Sequence
{
private readonly Asn1Object _op;
/// <summary>
/// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
/// Create an RfcLdapMessage request from input parameters.
/// </summary>
/// <param name="op">The op.</param>
/// <param name="controls">The controls.</param>
public RfcLdapMessage(IRfcRequest op, RfcControls controls)
: base(3)
{
_op = (Asn1Object) op;
Add(new RfcMessageID()); // MessageID has static counter
Add((Asn1Object) op);
if (controls != null)
{
Add(controls);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="RfcLdapMessage"/> class.
/// Will decode an RfcLdapMessage directly from an InputStream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="len">The length.</param>
/// <exception cref="Exception">RfcLdapMessage: Invalid tag: " + protocolOpId.Tag.</exception>
public RfcLdapMessage(Stream stream, int len)
: base(stream, len)
{
// Decode implicitly tagged protocol operation from an Asn1Tagged type
// to its appropriate application type.
var protocolOp = (Asn1Tagged) Get(1);
var protocolOpId = protocolOp.GetIdentifier();
var content = ((Asn1OctetString) protocolOp.TaggedValue).ByteValue();
var bais = new MemoryStream(content.ToByteArray());
switch ((LdapOperation) protocolOpId.Tag)
{
case LdapOperation.SearchResponse:
Set(1, new RfcSearchResultEntry(bais, content.Length));
break;
case LdapOperation.SearchResult:
Set(1, new RfcSearchResultDone(bais, content.Length));
break;
case LdapOperation.SearchResultReference:
Set(1, new RfcSearchResultReference(bais, content.Length));
break;
case LdapOperation.BindResponse:
Set(1, new RfcBindResponse(bais, content.Length));
break;
case LdapOperation.ExtendedResponse:
Set(1, new RfcExtendedResponse(bais, content.Length));
break;
case LdapOperation.IntermediateResponse:
Set(1, new RfcIntermediateResponse(bais, content.Length));
break;
case LdapOperation.ModifyResponse:
Set(1, new RfcModifyResponse(bais, content.Length));
break;
default:
throw new InvalidOperationException($"RfcLdapMessage: Invalid tag: {protocolOpId.Tag}");
}
// decode optional implicitly tagged controls from Asn1Tagged type to
// to RFC 2251 types.
if (Size() <= 2) return;
var controls = (Asn1Tagged) Get(2);
content = ((Asn1OctetString) controls.TaggedValue).ByteValue();
using (var ms = new MemoryStream(content.ToByteArray()))
Set(2, new RfcControls(ms, content.Length));
}
public int MessageId => ((Asn1Integer) Get(0)).IntValue();
/// <summary> Returns this RfcLdapMessage's message type.</summary>
public LdapOperation Type => (LdapOperation) Get(1).GetIdentifier().Tag;
public Asn1Object Response => Get(1);
public string RequestDn => ((IRfcRequest) _op).GetRequestDN();
public LdapMessage RequestingMessage { get; set; }
public IRfcRequest GetRequest() => (IRfcRequest) Get(1);
public bool IsRequest() => Get(1) is IRfcRequest;
}
/// <summary>
/// Represents Ldap Controls.
/// <pre>
/// Controls ::= SEQUENCE OF Control
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1SequenceOf" />
internal class RfcControls : Asn1SequenceOf
{
public const int Controls = 0;
public RfcControls()
: base(5)
{
}
public RfcControls(Stream stream, int len)
: base(stream, len)
{
// Convert each SEQUENCE element to a Control
for (var i = 0; i < Size(); i++)
{
var tempControl = new RfcControl((Asn1Sequence) Get(i));
Set(i, tempControl);
}
}
public void Add(RfcControl control) => base.Add(control);
public void Set(int index, RfcControl control) => base.Set(index, control);
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(Controls, true);
}
/// <summary>
/// This interface represents RfcLdapMessages that contain a response from a
/// server.
/// If the protocol operation of the RfcLdapMessage is of this type,
/// it contains at least an RfcLdapResult.
/// </summary>
internal interface IRfcResponse
{
/// <summary>
/// Gets the result code.
/// </summary>
/// <returns>Asn1Enumerated.</returns>
Asn1Enumerated GetResultCode();
/// <summary>
/// Gets the matched dn.
/// </summary>
/// <returns>RfcLdapDN.</returns>
Asn1OctetString GetMatchedDN();
/// <summary>
/// Gets the error message.
/// </summary>
/// <returns>RfcLdapString.</returns>
Asn1OctetString GetErrorMessage();
/// <summary>
/// Gets the referral.
/// </summary>
/// <returns>Asn1SequenceOf.</returns>
Asn1SequenceOf GetReferral();
}
/// <summary>
/// This interface represents Protocol Operations that are requests from a
/// client.
/// </summary>
internal interface IRfcRequest
{
/// <summary>
/// Builds a new request using the data from the this object.
/// </summary>
/// <returns>A <see cref="System.String" />.</returns>
string GetRequestDN();
}
/// <summary>
/// Represents an LdapResult.
/// <pre>
/// LdapResult ::= SEQUENCE {
/// resultCode ENUMERATED {
/// success (0),
/// operationsError (1),
/// protocolError (2),
/// timeLimitExceeded (3),
/// sizeLimitExceeded (4),
/// compareFalse (5),
/// compareTrue (6),
/// authMethodNotSupported (7),
/// strongAuthRequired (8),
/// -- 9 reserved --
/// referral (10), -- new
/// adminLimitExceeded (11), -- new
/// unavailableCriticalExtension (12), -- new
/// confidentialityRequired (13), -- new
/// saslBindInProgress (14), -- new
/// noSuchAttribute (16),
/// undefinedAttributeType (17),
/// inappropriateMatching (18),
/// constraintViolation (19),
/// attributeOrValueExists (20),
/// invalidAttributeSyntax (21),
/// -- 22-31 unused --
/// noSuchObject (32),
/// aliasProblem (33),
/// invalidDNSyntax (34),
/// -- 35 reserved for undefined isLeaf --
/// aliasDereferencingProblem (36),
/// -- 37-47 unused --
/// inappropriateAuthentication (48),
/// invalidCredentials (49),
/// insufficientAccessRights (50),
/// busy (51),
/// unavailable (52),
/// unwillingToPerform (53),
/// loopDetect (54),
/// -- 55-63 unused --
/// namingViolation (64),
/// objectClassViolation (65),
/// notAllowedOnNonLeaf (66),
/// notAllowedOnRDN (67),
/// entryAlreadyExists (68),
/// objectClassModsProhibited (69),
/// -- 70 reserved for CLdap --
/// affectsMultipleDSAs (71), -- new
/// -- 72-79 unused --
/// other (80) },
/// -- 81-90 reserved for APIs --
/// matchedDN LdapDN,
/// errorMessage LdapString,
/// referral [3] Referral OPTIONAL }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcResponse" />
internal class RfcLdapResult : Asn1Sequence, IRfcResponse
{
public const int Referral = 3;
public RfcLdapResult(Stream stream, int len)
: base(stream, len)
{
// Decode optional referral from Asn1OctetString to Referral.
if (Size() <= 3) return;
var obj = (Asn1Tagged) Get(3);
var id = obj.GetIdentifier();
if (id.Tag != Referral) return;
var content = ((Asn1OctetString) obj.TaggedValue).ByteValue();
Set(3, new Asn1SequenceOf(new MemoryStream(content.ToByteArray()), content.Length));
}
public Asn1Enumerated GetResultCode() => (Asn1Enumerated) Get(0);
public Asn1OctetString GetMatchedDN() => new Asn1OctetString(((Asn1OctetString) Get(1)).ByteValue());
public Asn1OctetString GetErrorMessage() => new Asn1OctetString(((Asn1OctetString) Get(2)).ByteValue());
public Asn1SequenceOf GetReferral() => Size() > 3 ? (Asn1SequenceOf) Get(3) : null;
}
/// <summary>
/// Represents an Ldap Search Result Done Response.
/// <pre>
/// SearchResultDone ::= [APPLICATION 5] LdapResult
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.RfcLdapResult" />
internal class RfcSearchResultDone : RfcLdapResult
{
public RfcSearchResultDone(Stream stream, int len)
: base(stream, len)
{
}
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResult);
}
/// <summary>
/// Represents an Ldap Search Result Entry.
/// <pre>
/// SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
/// objectName LdapDN,
/// attributes PartialAttributeList }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
internal sealed class RfcSearchResultEntry : Asn1Sequence
{
public RfcSearchResultEntry(Stream stream, int len)
: base(stream, len)
{
}
public string ObjectName => ((Asn1OctetString) Get(0)).StringValue();
public Asn1Sequence Attributes => (Asn1Sequence) Get(1);
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.SearchResponse);
}
/// <summary>
/// Represents an Ldap Message ID.
/// <pre>
/// MessageID ::= INTEGER (0 .. maxInt)
/// maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
/// Note: The creation of a MessageID should be hidden within the creation of
/// an RfcLdapMessage. The MessageID needs to be in sequence, and has an
/// upper and lower limit. There is never a case when a user should be
/// able to specify the MessageID for an RfcLdapMessage. The MessageID()
/// class should be package protected. (So the MessageID value isn't
/// arbitrarily run up.)
/// </pre></summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Integer" />
internal class RfcMessageID : Asn1Integer
{
private static int _messageId;
private static readonly object SyncRoot = new object();
/// <summary>
/// Initializes a new instance of the <see cref="RfcMessageID"/> class.
/// Creates a MessageID with an auto incremented Asn1Integer value.
/// Bounds: (0 .. 2,147,483,647) (2^^31 - 1 or Integer.MAX_VALUE)
/// MessageID zero is never used in this implementation. Always
/// start the messages with one.
/// </summary>
protected internal RfcMessageID()
: base(MessageId)
{
}
private static int MessageId
{
get
{
lock (SyncRoot)
{
return _messageId < int.MaxValue ? ++_messageId : (_messageId = 1);
}
}
}
}
}
@@ -0,0 +1,46 @@
namespace Unosquare.Swan.Networking.Ldap
{
using System.IO;
/// <summary>
/// Represents an Ldap Modify Request.
/// <pre>
/// ModifyRequest ::= [APPLICATION 6] SEQUENCE {
/// object LdapDN,
/// modification SEQUENCE OF SEQUENCE {
/// operation ENUMERATED {
/// add (0),
/// delete (1),
/// replace (2) },
/// modification AttributeTypeAndValues } }
/// </pre>
/// </summary>
/// <seealso cref="Unosquare.Swan.Networking.Ldap.Asn1Sequence" />
/// <seealso cref="Unosquare.Swan.Networking.Ldap.IRfcRequest" />
internal sealed class RfcModifyRequest
: Asn1Sequence, IRfcRequest
{
public RfcModifyRequest(string obj, Asn1SequenceOf modification)
: base(2)
{
Add(obj);
Add(modification);
}
public Asn1SequenceOf Modifications => (Asn1SequenceOf)Get(1);
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyRequest);
public string GetRequestDN() => ((Asn1OctetString)Get(0)).StringValue();
}
internal class RfcModifyResponse : RfcLdapResult
{
public RfcModifyResponse(Stream stream, int len)
: base(stream, len)
{
}
public override Asn1Identifier GetIdentifier() => new Asn1Identifier(LdapOperation.ModifyResponse);
}
}