USM Beta
parent
3a084820f5
commit
59133a9c8f
|
@ -33,6 +33,11 @@ namespace ln.snmp
|
||||||
Number = number;
|
Number = number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Is(IdentifierClass identifierClass, bool constructed, ulong number)
|
||||||
|
{
|
||||||
|
return ((IdentifierClass == identifierClass) && (Constructed == constructed) && (Number == number));
|
||||||
|
}
|
||||||
|
|
||||||
public byte Firstbyte
|
public byte Firstbyte
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -49,6 +54,9 @@ namespace ln.snmp
|
||||||
{
|
{
|
||||||
return String.Format("[ASN.1 Type Class={0} Constructed={1} Number={2} FirstByte=0x{3,2:X}]",IdentifierClass,Constructed,Number,Firstbyte);
|
return String.Format("[ASN.1 Type Class={0} Constructed={1} Number={2} FirstByte=0x{3,2:X}]",IdentifierClass,Constructed,Number,Firstbyte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BasicEncodingRules
|
public static class BasicEncodingRules
|
||||||
|
@ -131,10 +139,16 @@ namespace ln.snmp
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
byte[] bytes = EncodeInteger(length);
|
byte[] bytes = BitConverter.GetBytes(length);
|
||||||
byte bl = (byte)(0x80 | bytes.Length);
|
int n;
|
||||||
|
for (n = 3; n > 0 && bytes[n] == 0; n--) { }
|
||||||
|
n++;
|
||||||
|
byte[] finalbytes = new byte[n];
|
||||||
|
Array.Copy(bytes, finalbytes, n);
|
||||||
|
|
||||||
|
byte bl = (byte)(0x80 | finalbytes.Length);
|
||||||
stream.WriteByte(bl);
|
stream.WriteByte(bl);
|
||||||
stream.Write(bytes, 0, bytes.Length);
|
stream.Write(finalbytes, 0, finalbytes.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,35 +67,22 @@ namespace ln.snmp
|
||||||
Logging.Log(LogLevel.DEBUG,"SNMPClient: Received: {0}", BitConverter.ToString(datagram));
|
Logging.Log(LogLevel.DEBUG,"SNMPClient: Received: {0}", BitConverter.ToString(datagram));
|
||||||
|
|
||||||
ASN1Value asn = new ASN1Value(datagram);
|
ASN1Value asn = new ASN1Value(datagram);
|
||||||
|
|
||||||
asn.Dump();
|
|
||||||
|
|
||||||
SnmpMessage snmpMessage = asn;
|
SnmpMessage snmpMessage = asn;
|
||||||
|
|
||||||
snmpMessage.Dump();
|
if (currentEndpoints.ContainsKey(remoteEndpoint))
|
||||||
|
|
||||||
lock (queuedRequests)
|
|
||||||
{
|
{
|
||||||
if (queuedRequests.ContainsKey(snmpMessage.MessageID))
|
SnmpEndpoint snmpEndpoint = currentEndpoints[remoteEndpoint];
|
||||||
{
|
snmpEndpoint.Received(snmpMessage);
|
||||||
SnmpMessage snmpRequestMessage = queuedRequests[snmpMessage.MessageID];
|
|
||||||
lock (snmpRequestMessage)
|
|
||||||
{
|
|
||||||
queuedRequests[snmpMessage.MessageID] = snmpMessage;
|
|
||||||
Monitor.PulseAll(snmpRequestMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logging.Log(LogLevel.WARNING, "SnmpEngine.Receiver(): Can't find pending request with MessageID {0} ( 0x{0:x8} )",snmpMessage.MessageID);
|
Logging.Log(LogLevel.WARNING, "SnmpEngine.Receiver(): Received message from unknown endpoint ({0})",remoteEndpoint);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (SocketException se)
|
catch (SocketException se)
|
||||||
{
|
{
|
||||||
Logging.Log(se);
|
//Logging.Log(se);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -115,40 +102,40 @@ namespace ln.snmp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SnmpMessage SNMPRequest(IPEndPoint remoteEndpoint,SnmpMessage snmpMessage,int timeout)
|
//public SnmpMessage SNMPRequest(IPEndPoint remoteEndpoint,SnmpMessage snmpMessage,int timeout)
|
||||||
{
|
//{
|
||||||
lock (queuedRequests)
|
// lock (queuedRequests)
|
||||||
{
|
// {
|
||||||
ASN1Value snmpRequest = snmpMessage;
|
// ASN1Value snmpRequest = snmpMessage;
|
||||||
byte[] snmpRequestBytes = snmpRequest.AsByteArray;
|
// byte[] snmpRequestBytes = snmpRequest.AsByteArray;
|
||||||
|
|
||||||
queuedRequests.Add(snmpMessage.MessageID, snmpMessage);
|
// queuedRequests.Add(snmpMessage.MessageID, snmpMessage);
|
||||||
|
|
||||||
Logging.Log(LogLevel.DEBUG,"SNMPClient: Send: {0}", BitConverter.ToString(snmpRequestBytes));
|
// Logging.Log(LogLevel.DEBUG,"SNMPClient: Send: {0}", BitConverter.ToString(snmpRequestBytes));
|
||||||
Logging.Log(LogLevel.DEBUG,"sent snmpMessage with MessageID {0} ( 0x{0:x8} )",snmpMessage.MessageID);
|
// Logging.Log(LogLevel.DEBUG,"sent snmpMessage with MessageID {0} ( 0x{0:x8} )",snmpMessage.MessageID);
|
||||||
snmpMessage.Dump();
|
// snmpMessage.Dump();
|
||||||
|
|
||||||
LocalEndpoint.Send(snmpRequestBytes, snmpRequestBytes.Length, remoteEndpoint);
|
// LocalEndpoint.Send(snmpRequestBytes, snmpRequestBytes.Length, remoteEndpoint);
|
||||||
|
|
||||||
lock (snmpMessage)
|
// lock (snmpMessage)
|
||||||
{
|
// {
|
||||||
Monitor.Exit(queuedRequests);
|
// Monitor.Exit(queuedRequests);
|
||||||
bool success = Monitor.Wait(snmpMessage, timeout);
|
// bool success = Monitor.Wait(snmpMessage, timeout);
|
||||||
Monitor.Enter(queuedRequests);
|
// Monitor.Enter(queuedRequests);
|
||||||
|
|
||||||
if (!success)
|
// if (!success)
|
||||||
{
|
// {
|
||||||
queuedRequests.Remove(snmpMessage.MessageID);
|
// queuedRequests.Remove(snmpMessage.MessageID);
|
||||||
throw new TimeoutException();
|
// throw new TimeoutException();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
SnmpMessage responseMessage = queuedRequests[snmpMessage.MessageID];
|
// SnmpMessage responseMessage = queuedRequests[snmpMessage.MessageID];
|
||||||
queuedRequests.Remove(snmpMessage.MessageID);
|
// queuedRequests.Remove(snmpMessage.MessageID);
|
||||||
|
|
||||||
return responseMessage;
|
// return responseMessage;
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
||||||
public void RegisterEndpoint(SnmpEndpoint intf)
|
public void RegisterEndpoint(SnmpEndpoint intf)
|
||||||
{
|
{
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace ln.snmp
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract PDU snmpRequest(PDU pdu);
|
public abstract PDU DispatchRequest(PDU pdu);
|
||||||
|
|
||||||
private List<Sequence> snmpRequest<T>(IEnumerable<ObjectIdentifier> objectIdentifiers) where T: PDU, new()
|
private List<Sequence> snmpRequest<T>(IEnumerable<ObjectIdentifier> objectIdentifiers) where T: PDU, new()
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,7 @@ namespace ln.snmp
|
||||||
pdu.Add(new Sequence(new Variable[] { oid, NullValue.Instance }));
|
pdu.Add(new Sequence(new Variable[] { oid, NullValue.Instance }));
|
||||||
}
|
}
|
||||||
|
|
||||||
PDU responsePDU = snmpRequest(pdu);
|
PDU responsePDU = DispatchRequest(pdu);
|
||||||
|
|
||||||
if (responsePDU.Error.LongValue != 0)
|
if (responsePDU.Error.LongValue != 0)
|
||||||
{
|
{
|
||||||
|
@ -84,7 +84,7 @@ namespace ln.snmp
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return snmpRequest(bulkRequest);
|
return DispatchRequest(bulkRequest);
|
||||||
} catch (SnmpError se)
|
} catch (SnmpError se)
|
||||||
{
|
{
|
||||||
if ((se.Error == 1)&&(bulkRequest.MaxRepetitions.LongValue > 1))
|
if ((se.Error == 1)&&(bulkRequest.MaxRepetitions.LongValue > 1))
|
||||||
|
@ -122,17 +122,19 @@ namespace ln.snmp
|
||||||
Variable[] values = responsePDU.VarBinds.Items;
|
Variable[] values = responsePDU.VarBinds.Items;
|
||||||
|
|
||||||
nchunk = values.Length / ncolumns;
|
nchunk = values.Length / ncolumns;
|
||||||
inTree = false;
|
|
||||||
|
|
||||||
for (int n = 0; n < nchunk; n++)
|
for (int n = 0; n < nchunk; n++)
|
||||||
{
|
{
|
||||||
|
inTree = false;
|
||||||
|
|
||||||
Sequence[] row = new Sequence[ncolumns];
|
Sequence[] row = new Sequence[ncolumns];
|
||||||
for (int c = 0; c < ncolumns; c++)
|
for (int c = 0; c < ncolumns; c++)
|
||||||
{
|
{
|
||||||
Sequence v = values[(n * ncolumns) + c] as Sequence;
|
Sequence v = values[(n * ncolumns) + c] as Sequence;
|
||||||
if (OIDs[c].Contains(v.Items[0] as ObjectIdentifier))
|
ObjectIdentifier oid = v.Items[0] as ObjectIdentifier;
|
||||||
|
if (OIDs[c].Contains(oid))
|
||||||
{
|
{
|
||||||
row[c] = v;
|
row[c] = new Sequence(new Variable[] { oid.IndexTo(OIDs[c]), v.Items[1] });
|
||||||
inTree = true;
|
inTree = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,7 +173,9 @@ namespace ln.snmp
|
||||||
{
|
{
|
||||||
ObjectIdentifier oid = ps.Items[0] as ObjectIdentifier;
|
ObjectIdentifier oid = ps.Items[0] as ObjectIdentifier;
|
||||||
if (objectIdentifier.Contains(oid))
|
if (objectIdentifier.Contains(oid))
|
||||||
results.Add(ps);
|
{
|
||||||
|
results.Add(new Sequence(new Variable[] { oid.IndexTo(objectIdentifier), ps.Items[1] }));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -259,9 +263,9 @@ namespace ln.snmp
|
||||||
return snmpWalk(new ObjectIdentifier(objectIdentifier));
|
return snmpWalk(new ObjectIdentifier(objectIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
SNMPEngine.UnregisterEndpoint(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
30818f0201033011020428e35a0c020300ffe3040105020103043b3039041680003a8c0434433a35453a30433a31343a32343a393502010e0203136f860407736b7974726f6e040cc12bbc3135344c067e3eb8890400303a041680003a8c0434433a35453a30433a31343a32343a39350400a11e020469bb65e80201000201003010300e060a2b0601020102020104040500
|
|
@ -15,27 +15,31 @@ namespace ln.snmp.endpoint
|
||||||
public SNMPEngine SNMPEngine { get; }
|
public SNMPEngine SNMPEngine { get; }
|
||||||
public IPEndPoint RemoteEndpoint { get; set; }
|
public IPEndPoint RemoteEndpoint { get; set; }
|
||||||
|
|
||||||
|
public int Retries { get; set; } = 3;
|
||||||
|
|
||||||
public SnmpEndpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint)
|
public SnmpEndpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint)
|
||||||
{
|
{
|
||||||
SNMPEngine = snmpEngine;
|
SNMPEngine = snmpEngine;
|
||||||
RemoteEndpoint = remoteEndpoint;
|
RemoteEndpoint = remoteEndpoint;
|
||||||
|
|
||||||
SNMPEngine.RegisterEndpoint(this);
|
SNMPEngine.RegisterEndpoint(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Send(SnmpMessage message)
|
public virtual void Send(SnmpMessage message)
|
||||||
{
|
{
|
||||||
|
//message.MessageID = SNMPEngine.NextMessageID;
|
||||||
|
//if (requestMessage.snmpPDU != null)
|
||||||
|
//{
|
||||||
|
// requestMessage.snmpPDU.RequestID.LongValue = SNMPEngine.NextMessageID;
|
||||||
|
//}
|
||||||
|
|
||||||
message = Encrypt(message);
|
message = Encrypt(message);
|
||||||
message = ApplyAuthentication(message);
|
message = ApplyAuthentication(message);
|
||||||
SNMPEngine.SendMessage(RemoteEndpoint, message);
|
SNMPEngine.SendMessage(RemoteEndpoint, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Received(SnmpMessage message)
|
public virtual void DispatchReceived(SnmpMessage message)
|
||||||
{
|
{
|
||||||
message = CheckAuthentication(message);
|
|
||||||
message = Decrypt(message);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
lock (queuedRequests)
|
lock (queuedRequests)
|
||||||
{
|
{
|
||||||
if (queuedRequests.ContainsKey(message.MessageID))
|
if (queuedRequests.ContainsKey(message.MessageID))
|
||||||
|
@ -54,11 +58,19 @@ namespace ln.snmp.endpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract SnmpMessage Encrypt(SnmpMessage message);
|
public virtual void Received(SnmpMessage message)
|
||||||
public abstract SnmpMessage ApplyAuthentication(SnmpMessage message);
|
{
|
||||||
|
message = CheckAuthentication(message);
|
||||||
|
message = Decrypt(message);
|
||||||
|
|
||||||
public abstract SnmpMessage CheckAuthentication(SnmpMessage message);
|
DispatchReceived(message);
|
||||||
public abstract SnmpMessage Decrypt(SnmpMessage message);
|
}
|
||||||
|
|
||||||
|
public virtual SnmpMessage Encrypt(SnmpMessage message) => message;
|
||||||
|
public virtual SnmpMessage ApplyAuthentication(SnmpMessage message) => message;
|
||||||
|
|
||||||
|
public virtual SnmpMessage CheckAuthentication(SnmpMessage message) => message;
|
||||||
|
public virtual SnmpMessage Decrypt(SnmpMessage message) => message;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests
|
* Requests
|
||||||
|
@ -67,8 +79,6 @@ namespace ln.snmp.endpoint
|
||||||
|
|
||||||
public SnmpMessage EnqueueRequest(SnmpMessage requestMessage)
|
public SnmpMessage EnqueueRequest(SnmpMessage requestMessage)
|
||||||
{
|
{
|
||||||
requestMessage.MessageID = SNMPEngine.NextMessageID;
|
|
||||||
|
|
||||||
lock (queuedRequests)
|
lock (queuedRequests)
|
||||||
{
|
{
|
||||||
queuedRequests.Add(requestMessage.MessageID, requestMessage);
|
queuedRequests.Add(requestMessage.MessageID, requestMessage);
|
||||||
|
|
|
@ -27,14 +27,13 @@ namespace ln.snmp.endpoint
|
||||||
CommunityString = new OctetString(communityString);
|
CommunityString = new OctetString(communityString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override PDU snmpRequest(PDU pdu)
|
public override PDU DispatchRequest(PDU pdu)
|
||||||
{
|
{
|
||||||
SnmpV1Message request = new SnmpV1Message();
|
SnmpV1Message request = new SnmpV1Message();
|
||||||
request.MessageID = SNMPEngine.NextMessageID;
|
|
||||||
request.snmpCommunity = CommunityString;
|
request.snmpCommunity = CommunityString;
|
||||||
request.snmpPDU = pdu;
|
request.snmpPDU = pdu;
|
||||||
|
|
||||||
SnmpV1Message response = SNMPEngine.SNMPRequest(RemoteEndpoint, request, SNMPEngine.Timeout) as SnmpV1Message;
|
SnmpV1Message response = EnqueueRequest(request) as SnmpV1Message;
|
||||||
return response.snmpPDU;
|
return response.snmpPDU;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,8 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ln.snmp.endpoint
|
namespace ln.snmp.endpoint
|
||||||
{
|
{
|
||||||
public class SnmpV2Endpoint : SnmpEndpoint
|
public class SnmpV2Endpoint : SnmpV1Endpoint
|
||||||
{
|
{
|
||||||
public OctetString CommunityString { get; set; }
|
|
||||||
|
|
||||||
public override SnmpVersion SnmpVersion => SnmpVersion.V2c;
|
public override SnmpVersion SnmpVersion => SnmpVersion.V2c;
|
||||||
|
|
||||||
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint)
|
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint)
|
||||||
|
@ -17,24 +15,19 @@ namespace ln.snmp.endpoint
|
||||||
CommunityString = "public";
|
CommunityString = "public";
|
||||||
}
|
}
|
||||||
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint, OctetString communityString)
|
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint, OctetString communityString)
|
||||||
: base(snmpEngine, remoteEndpoint)
|
: base(snmpEngine, remoteEndpoint, communityString)
|
||||||
{
|
{ }
|
||||||
CommunityString = communityString;
|
|
||||||
}
|
|
||||||
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint, String communityString)
|
public SnmpV2Endpoint(SNMPEngine snmpEngine, IPEndPoint remoteEndpoint, String communityString)
|
||||||
: base(snmpEngine, remoteEndpoint)
|
: base(snmpEngine, remoteEndpoint, communityString)
|
||||||
{
|
{ }
|
||||||
CommunityString = new OctetString(communityString);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override PDU snmpRequest(PDU pdu)
|
public override PDU DispatchRequest(PDU pdu)
|
||||||
{
|
{
|
||||||
SnmpV2Message request = new SnmpV2Message();
|
SnmpV2Message request = new SnmpV2Message();
|
||||||
request.MessageID = SNMPEngine.NextMessageID;
|
|
||||||
request.snmpCommunity = CommunityString;
|
request.snmpCommunity = CommunityString;
|
||||||
request.snmpPDU = pdu;
|
request.snmpPDU = pdu;
|
||||||
|
|
||||||
SnmpV2Message response = SNMPEngine.SNMPRequest(RemoteEndpoint, request, SNMPEngine.Timeout) as SnmpV2Message;
|
SnmpV2Message response = EnqueueRequest(request) as SnmpV2Message;
|
||||||
return response.snmpPDU;
|
return response.snmpPDU;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ using System.Security.Cryptography;
|
||||||
using ln.snmp.asn1;
|
using ln.snmp.asn1;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ln.logging;
|
using ln.logging;
|
||||||
|
using System.Threading;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace ln.snmp.endpoint
|
namespace ln.snmp.endpoint
|
||||||
{
|
{
|
||||||
|
@ -32,11 +34,61 @@ namespace ln.snmp.endpoint
|
||||||
|
|
||||||
public override SnmpVersion SnmpVersion => SnmpVersion.V3;
|
public override SnmpVersion SnmpVersion => SnmpVersion.V3;
|
||||||
|
|
||||||
|
private object reportDispatchLock = new object();
|
||||||
|
|
||||||
public USMEndpoint(SNMPEngine snmpEngine,IPEndPoint remoteEndpoint)
|
public USMEndpoint(SNMPEngine snmpEngine,IPEndPoint remoteEndpoint)
|
||||||
:base(snmpEngine,remoteEndpoint)
|
:base(snmpEngine,remoteEndpoint)
|
||||||
{
|
{
|
||||||
|
CacheAuthoritativeEngineTime = new Integer();
|
||||||
|
CacheAuthoritativeEngineBoots = new Integer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void DispatchReceived(SnmpMessage message)
|
||||||
|
{
|
||||||
|
Logging.Log(LogLevel.DEBUG, "Received PDU: {0}",message.snmpPDU);
|
||||||
|
|
||||||
|
USMMessage usm = message as USMMessage;
|
||||||
|
|
||||||
|
if (usm.SecurityParameters.msgAuthoritativeEngineBoots.LongValue > 0)
|
||||||
|
CacheAuthoritativeEngineBoots.LongValue = usm.SecurityParameters.msgAuthoritativeEngineBoots.LongValue;
|
||||||
|
if (usm.SecurityParameters.msgAuthoritativeEngineTime.LongValue > 0)
|
||||||
|
CacheAuthoritativeEngineTime.LongValue = usm.SecurityParameters.msgAuthoritativeEngineTime.LongValue;
|
||||||
|
|
||||||
|
if (message.snmpPDU is Report)
|
||||||
|
{
|
||||||
|
DispatchReport(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.DispatchReceived(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DispatchReport(SnmpMessage snmpMessage)
|
||||||
|
{
|
||||||
|
USMMessage usm = snmpMessage as USMMessage;
|
||||||
|
|
||||||
|
RemoteEngineID = usm.SecurityParameters.msgAuthoritativeEngineID;
|
||||||
|
|
||||||
|
|
||||||
|
lock (reportDispatchLock)
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(reportDispatchLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SnmpMessage ApplyAuthentication(SnmpMessage message)
|
||||||
|
{
|
||||||
|
if (RemoteEngineID == null)
|
||||||
|
return message;
|
||||||
|
|
||||||
|
if (AuthenticateMessage(message as USMMessage))
|
||||||
|
return message;
|
||||||
|
|
||||||
|
throw new Exception("Authentication failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public SnmpAuthLevel AuthLevel
|
public SnmpAuthLevel AuthLevel
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -50,13 +102,16 @@ namespace ln.snmp.endpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override PDU snmpRequest(PDU pdu)
|
public override PDU DispatchRequest(PDU pdu)
|
||||||
{
|
{
|
||||||
if ((RemoteEngineID == null) || (RemoteEngineID.Bytes.Length == 0))
|
if ((RemoteEngineID == null) || (RemoteEngineID.Bytes.Length == 0))
|
||||||
{
|
{
|
||||||
QueryEngineID();
|
QueryEngineID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int retries = Retries;
|
||||||
|
while (retries-- > 0)
|
||||||
|
{
|
||||||
USMMessage request = new USMMessage();
|
USMMessage request = new USMMessage();
|
||||||
ScopedPDU scopedPDU = new ScopedPDU();
|
ScopedPDU scopedPDU = new ScopedPDU();
|
||||||
|
|
||||||
|
@ -64,25 +119,34 @@ namespace ln.snmp.endpoint
|
||||||
|
|
||||||
request.msgData = scopedPDU;
|
request.msgData = scopedPDU;
|
||||||
|
|
||||||
|
if (RemoteEngineID == null)
|
||||||
|
{
|
||||||
|
scopedPDU.contextEngineID = new OctetString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
scopedPDU.contextEngineID = RemoteEngineID;
|
scopedPDU.contextEngineID = RemoteEngineID;
|
||||||
scopedPDU.PDU = pdu;
|
|
||||||
|
|
||||||
AuthenticateMessage(request);
|
|
||||||
|
|
||||||
USMMessage replyUSM = InternalRequest(request);
|
|
||||||
|
|
||||||
PDU responsePDU = (replyUSM.msgData as ScopedPDU).PDU;
|
|
||||||
|
|
||||||
return responsePDU;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private USMMessage InternalRequest(USMMessage request)
|
scopedPDU.PDU = pdu;
|
||||||
{
|
|
||||||
SnmpMessage reply = SNMPEngine.SNMPRequest(RemoteEndpoint, request, SNMPEngine.Timeout);
|
|
||||||
USMMessage usmReply = reply as USMMessage;
|
|
||||||
|
|
||||||
CacheAuthoritativeEngineBoots = usmReply.SecurityParameters.msgAuthoritativeEngineBoots;
|
try
|
||||||
CacheAuthoritativeEngineTime = usmReply.SecurityParameters.msgAuthoritativeEngineTime;
|
{
|
||||||
|
USMMessage replyUSM = DispatchRequest(request);
|
||||||
|
PDU responsePDU = (replyUSM.msgData as ScopedPDU).PDU;
|
||||||
|
return responsePDU;
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new TimeoutException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private USMMessage DispatchRequest(USMMessage request)
|
||||||
|
{
|
||||||
|
SnmpMessage reply = EnqueueRequest(request);
|
||||||
|
USMMessage usmReply = reply as USMMessage;
|
||||||
|
|
||||||
return usmReply;
|
return usmReply;
|
||||||
}
|
}
|
||||||
|
@ -96,26 +160,25 @@ namespace ln.snmp.endpoint
|
||||||
|
|
||||||
queryMessage.msgData = scopedPDU;
|
queryMessage.msgData = scopedPDU;
|
||||||
|
|
||||||
queryMessage.Dump();
|
lock (reportDispatchLock)
|
||||||
|
{
|
||||||
USMMessage usmReply = InternalRequest(queryMessage);
|
Send(queryMessage);
|
||||||
|
if (!Monitor.Wait(reportDispatchLock, SNMPEngine.Timeout))
|
||||||
RemoteEngineID = usmReply.SecurityParameters.msgAuthoritativeEngineID;
|
throw new TimeoutException("could not query engine id");
|
||||||
//RemoteEngineID.Bytes = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 };
|
}
|
||||||
|
|
||||||
LocalizeKeys();
|
LocalizeKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AuthenticateMessage(USMMessage message)
|
public bool AuthenticateMessage(USMMessage message)
|
||||||
{
|
{
|
||||||
message.msgGlobalData.msgFlags.Bytes = new byte[] { (byte)(int)AuthLevel };
|
message.msgGlobalData.msgFlags.Bytes[0] |= 0x01;
|
||||||
message.SecurityParameters.msgUserName.StringValue = Username;
|
message.SecurityParameters.msgUserName.StringValue = Username;
|
||||||
message.SecurityParameters.msgAuthoritativeEngineID = RemoteEngineID;
|
message.SecurityParameters.msgAuthoritativeEngineID = RemoteEngineID == null ? new OctetString() : RemoteEngineID;
|
||||||
message.SecurityParameters.msgAuthenticationParameters.Bytes = new byte[12];
|
message.SecurityParameters.msgAuthenticationParameters.Bytes = new byte[12];
|
||||||
message.SecurityParameters.msgAuthoritativeEngineBoots = CacheAuthoritativeEngineBoots != null ? CacheAuthoritativeEngineBoots : new Integer();
|
message.SecurityParameters.msgAuthoritativeEngineBoots = CacheAuthoritativeEngineBoots != null ? CacheAuthoritativeEngineBoots : new Integer();
|
||||||
message.SecurityParameters.msgAuthoritativeEngineTime = CacheAuthoritativeEngineTime != null ? CacheAuthoritativeEngineTime : new Integer();
|
message.SecurityParameters.msgAuthoritativeEngineTime = CacheAuthoritativeEngineTime != null ? CacheAuthoritativeEngineTime : new Integer();
|
||||||
|
|
||||||
|
|
||||||
byte[] wholeMsg = ((ASN1Value)message).AsByteArray;
|
byte[] wholeMsg = ((ASN1Value)message).AsByteArray;
|
||||||
|
|
||||||
byte[] extendedAuthKey = new byte[64];
|
byte[] extendedAuthKey = new byte[64];
|
||||||
|
@ -146,9 +209,23 @@ namespace ln.snmp.endpoint
|
||||||
|
|
||||||
byte[] mac = hash.ComputeHash(inter2);
|
byte[] mac = hash.ComputeHash(inter2);
|
||||||
|
|
||||||
|
|
||||||
|
Logging.Log(LogLevel.DEBUG, "Authentication of {0}", BitConverter.ToString(wholeMsg));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "AuthKey: {0}", BitConverter.ToString(AuthKey));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "LocalAuthKey: {0}", BitConverter.ToString(LocalAuthKey));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "Extended AuthKey: {0}", BitConverter.ToString(extendedAuthKey));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "");
|
||||||
|
Logging.Log(LogLevel.DEBUG, "K1: {0}", BitConverter.ToString(K1));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "K2: {0}", BitConverter.ToString(K2));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "");
|
||||||
|
Logging.Log(LogLevel.DEBUG, "AuthToken: {0}", BitConverter.ToString(mac.Take(12).ToArray()));
|
||||||
|
|
||||||
message.SecurityParameters.msgAuthenticationParameters.Bytes = mac.Take(12).ToArray();
|
message.SecurityParameters.msgAuthenticationParameters.Bytes = mac.Take(12).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logging.Log(LogLevel.DEBUG, "Authenticating Message: {0}",BitConverter.ToString(wholeMsg));
|
||||||
|
Logging.Log(LogLevel.DEBUG, "Authenticated Message: {0}", BitConverter.ToString(((ASN1Value)message).AsByteArray));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +257,7 @@ namespace ln.snmp.endpoint
|
||||||
return AuthMethod == SnmpV3AuthMethod.SHA ? (HashAlgorithm)SHA1.Create() : (HashAlgorithm)MD5.Create();
|
return AuthMethod == SnmpV3AuthMethod.SHA ? (HashAlgorithm)SHA1.Create() : (HashAlgorithm)MD5.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LocalizeKeys()
|
public void LocalizeKeys()
|
||||||
{
|
{
|
||||||
if ((AuthKey != null) && (RemoteEngineID != null))
|
if ((AuthKey != null) && (RemoteEngineID != null))
|
||||||
{
|
{
|
||||||
|
|
|
@ -58,11 +58,14 @@
|
||||||
<Compile Include="types\UsmSecurityParameters.cs" />
|
<Compile Include="types\UsmSecurityParameters.cs" />
|
||||||
<Compile Include="endpoint\SNMPEndpoint.cs" />
|
<Compile Include="endpoint\SNMPEndpoint.cs" />
|
||||||
<Compile Include="SNMPInterface.cs" />
|
<Compile Include="SNMPInterface.cs" />
|
||||||
|
<Compile Include="rfc1213\RFC1213.cs" />
|
||||||
|
<Compile Include="types\IPAddr.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="types\" />
|
<Folder Include="types\" />
|
||||||
<Folder Include="endpoint\" />
|
<Folder Include="endpoint\" />
|
||||||
<Folder Include="asn1\" />
|
<Folder Include="asn1\" />
|
||||||
|
<Folder Include="rfc1213\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ln.logging\ln.logging.csproj">
|
<ProjectReference Include="..\ln.logging\ln.logging.csproj">
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
// /**
|
||||||
|
// * File: RFC1213.cs
|
||||||
|
// * Author: haraldwolff
|
||||||
|
// *
|
||||||
|
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||||
|
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||||
|
// *
|
||||||
|
// *
|
||||||
|
// **/
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using ln.snmp.endpoint;
|
||||||
|
using ln.snmp.types;
|
||||||
|
using System.Linq;
|
||||||
|
namespace ln.snmp.rfc1213
|
||||||
|
{
|
||||||
|
public static class RFC1213
|
||||||
|
{
|
||||||
|
|
||||||
|
public static Interface[] GetInterfaces(SnmpEndpoint endpoint)
|
||||||
|
{
|
||||||
|
Dictionary<int,Interface> interfaces = new Dictionary<int,Interface>();
|
||||||
|
|
||||||
|
Sequence[][] ifTable = endpoint.snmpWalk(new String[] {
|
||||||
|
"1.3.6.1.2.1.2.2.1.2",
|
||||||
|
"1.3.6.1.2.1.2.2.1.10",
|
||||||
|
"1.3.6.1.2.1.2.2.1.16",
|
||||||
|
"1.3.6.1.2.1.2.2.1.14",
|
||||||
|
"1.3.6.1.2.1.2.2.1.20"
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (Sequence[] row in ifTable)
|
||||||
|
{
|
||||||
|
ObjectIdentifier index = (row[0].Items[0] as ObjectIdentifier);
|
||||||
|
Interface intf = new Interface();
|
||||||
|
intf.Name = (row[0].Items[1] as OctetString).StringValue;
|
||||||
|
interfaces.Add(index.OIDValue[0],intf);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sequence[][] ipTable = endpoint.snmpWalk(new String[]
|
||||||
|
{
|
||||||
|
"1.3.6.1.2.1.4.20.1.1",
|
||||||
|
"1.3.6.1.2.1.4.20.1.2"
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (Sequence[] row in ipTable)
|
||||||
|
{
|
||||||
|
IPAddress ip = (row[0].Items[1] as IPAddr).IP;
|
||||||
|
int ifIndex = (int)(row[1].Items[1] as Integer).LongValue;
|
||||||
|
|
||||||
|
if (interfaces.ContainsKey(ifIndex))
|
||||||
|
{
|
||||||
|
interfaces[ifIndex].AddIPAddress(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return interfaces.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class Interface
|
||||||
|
{
|
||||||
|
public String Name { get; set; }
|
||||||
|
public System.Net.IPAddress[] IPAddresses => _IPAddresses.ToArray();
|
||||||
|
|
||||||
|
|
||||||
|
private List<System.Net.IPAddress> _IPAddresses = new List<System.Net.IPAddress>();
|
||||||
|
|
||||||
|
|
||||||
|
public Interface()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddIPAddress(System.Net.IPAddress ip)
|
||||||
|
{
|
||||||
|
_IPAddresses.Add(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return String.Format("[Interface Name={0} IPAddresses=({1})]",Name,String.Join(", ",IPAddresses.Select((x)=>x.ToString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// /**
|
||||||
|
// * File: IPAddress.cs
|
||||||
|
// * Author: haraldwolff
|
||||||
|
// *
|
||||||
|
// * This file and it's content is copyrighted by the Author and / or copyright holder.
|
||||||
|
// * Any use wihtout proper permission is illegal and may lead to legal actions.
|
||||||
|
// *
|
||||||
|
// *
|
||||||
|
// **/
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
namespace ln.snmp.types
|
||||||
|
{
|
||||||
|
public class IPAddr : Variable
|
||||||
|
{
|
||||||
|
public System.Net.IPAddress IP { get; set; }
|
||||||
|
|
||||||
|
public IPAddr()
|
||||||
|
:base(new Identifier(IdentifierClass.APPLICATION,false,0x00))
|
||||||
|
{
|
||||||
|
IP = new IPAddress(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override byte[] Bytes {
|
||||||
|
get => IP.GetAddressBytes();
|
||||||
|
set => IP = new System.Net.IPAddress(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object Value {
|
||||||
|
get => throw new NotImplementedException();
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,19 @@ namespace ln.snmp.types
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectIdentifier IndexTo(ObjectIdentifier b)
|
||||||
|
{
|
||||||
|
if (!b.Contains(this))
|
||||||
|
throw new FormatException("OID is not below base OID");
|
||||||
|
|
||||||
|
int[] me = OIDValue;
|
||||||
|
int[] you = b.OIDValue;
|
||||||
|
|
||||||
|
int[] result = new int[me.Length - you.Length];
|
||||||
|
Array.Copy(me, you.Length, result, 0, result.Length);
|
||||||
|
|
||||||
|
return new ObjectIdentifier(result);
|
||||||
|
}
|
||||||
|
|
||||||
public override byte[] Bytes
|
public override byte[] Bytes
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,26 +20,21 @@ namespace ln.snmp.types
|
||||||
|
|
||||||
public Sequence VarBinds { get; private set; }
|
public Sequence VarBinds { get; private set; }
|
||||||
|
|
||||||
private Variable[] items;
|
|
||||||
|
|
||||||
public PDU(Identifier identifier)
|
public PDU(Identifier identifier)
|
||||||
: base(identifier)
|
: base(identifier)
|
||||||
{
|
{
|
||||||
//RequestID = new Integer(Environment.TickCount);
|
//RequestID = new Integer(Environment.TickCount);
|
||||||
RequestID = new Integer(0xDEADBEEF);
|
RequestID = new Integer((int)(DateTimeOffset.Now.ToUnixTimeMilliseconds()) & 0x7FFFFFFF);
|
||||||
Error = new Integer();
|
Error = new Integer();
|
||||||
ErrorIndex = new Integer();
|
ErrorIndex = new Integer();
|
||||||
|
|
||||||
VarBinds = new Sequence();
|
VarBinds = new Sequence();
|
||||||
|
|
||||||
items = new Variable[] { RequestID, Error, ErrorIndex, VarBinds };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PDU(ASN1Value value)
|
public PDU(ASN1Value value)
|
||||||
: base(value.Identifier)
|
: base(value.Identifier)
|
||||||
{
|
{
|
||||||
RequestID = (Integer)value.Items[0];
|
RequestID = (Integer)value.Items[0];
|
||||||
|
|
||||||
Error = (Integer)value.Items[1];
|
Error = (Integer)value.Items[1];
|
||||||
Error = (Integer)value.Items[2];
|
Error = (Integer)value.Items[2];
|
||||||
VarBinds = (Sequence)value.Items[3];
|
VarBinds = (Sequence)value.Items[3];
|
||||||
|
@ -47,7 +42,7 @@ namespace ln.snmp.types
|
||||||
|
|
||||||
public override Variable[] Items
|
public override Variable[] Items
|
||||||
{
|
{
|
||||||
get => items;
|
get => new Variable[] { RequestID, Error, ErrorIndex, VarBinds };
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
RequestID = value[0] as Integer;
|
RequestID = value[0] as Integer;
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
// **/
|
// **/
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.Remoting.Messaging;
|
using System.Runtime.Remoting.Messaging;
|
||||||
|
using ln.snmp.asn1;
|
||||||
|
using System.Linq;
|
||||||
namespace ln.snmp.types
|
namespace ln.snmp.types
|
||||||
{
|
{
|
||||||
public class ScopedPDU : AbstractSequence
|
public class ScopedPDU : AbstractSequence
|
||||||
|
@ -26,6 +28,12 @@ namespace ln.snmp.types
|
||||||
PDU = new GetRequest();
|
PDU = new GetRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ScopedPDU(ASN1Value asn)
|
||||||
|
:this()
|
||||||
|
{
|
||||||
|
Items = asn.Items.Select((x) => (Variable)x).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
public override Variable[] Items
|
public override Variable[] Items
|
||||||
{
|
{
|
||||||
get => new Variable[] { contextEngineID, contextName, PDU };
|
get => new Variable[] { contextEngineID, contextName, PDU };
|
||||||
|
|
|
@ -60,8 +60,14 @@ namespace ln.snmp.types
|
||||||
msgGlobalData = new MsgGlobalData(value[1]);
|
msgGlobalData = new MsgGlobalData(value[1]);
|
||||||
SecurityParameters = new UsmSecurityParameters(new ASN1Value(value[2].Bytes));
|
SecurityParameters = new UsmSecurityParameters(new ASN1Value(value[2].Bytes));
|
||||||
|
|
||||||
// ToDo: Check if value[3] is OctetString (Encrypted) or Sequence (ScopedPDU)
|
if (value[3].Identifier.Is(IdentifierClass.UNIVERSAL,false,0x04))
|
||||||
msgData = value[3];
|
{
|
||||||
|
EncryptedPDU = value[3] as OctetString;
|
||||||
|
msgData = EncryptedPDU;
|
||||||
|
} else {
|
||||||
|
ScopedPDU = new ScopedPDU(value[3]);
|
||||||
|
msgData = ScopedPDU;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,13 +98,13 @@ namespace ln.snmp.types
|
||||||
{
|
{
|
||||||
msgID = new Integer((int)(DateTimeOffset.Now.ToUnixTimeSeconds()));
|
msgID = new Integer((int)(DateTimeOffset.Now.ToUnixTimeSeconds()));
|
||||||
msgMaxSize = new Integer(65000);
|
msgMaxSize = new Integer(65000);
|
||||||
msgFlags = new OctetString("\0");
|
msgFlags = new OctetString(new byte[] { 0x04 });
|
||||||
msgSecurityModel = new Integer(3);
|
msgSecurityModel = new Integer(3);
|
||||||
}
|
}
|
||||||
public MsgGlobalData(ASN1Value asn)
|
public MsgGlobalData(ASN1Value asn)
|
||||||
:this()
|
:this()
|
||||||
{
|
{
|
||||||
|
Items = asn.Items.ToVariableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Add(Variable item) => throw new NotImplementedException();
|
public override void Add(Variable item) => throw new NotImplementedException();
|
||||||
|
|
|
@ -125,6 +125,8 @@ namespace ln.snmp.types
|
||||||
registerKnownType<Sequence>();
|
registerKnownType<Sequence>();
|
||||||
registerKnownType<ObjectIdentifier>();
|
registerKnownType<ObjectIdentifier>();
|
||||||
|
|
||||||
|
registerKnownType<IPAddr>();
|
||||||
|
|
||||||
registerKnownType<GetRequest>();
|
registerKnownType<GetRequest>();
|
||||||
registerKnownType<GetNextRequest>();
|
registerKnownType<GetNextRequest>();
|
||||||
registerKnownType<GetResponse>();
|
registerKnownType<GetResponse>();
|
||||||
|
@ -135,4 +137,15 @@ namespace ln.snmp.types
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class VariableExtensions
|
||||||
|
{
|
||||||
|
|
||||||
|
public static Variable[] ToVariableArray(this ASN1Value[] me)
|
||||||
|
{
|
||||||
|
return me.Select((x) => (Variable)x).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue