diff --git a/EnergySoultions.CoAP.sln b/EnergySoultions.CoAP.sln index cdddf2f..532ebd0 100644 --- a/EnergySoultions.CoAP.sln +++ b/EnergySoultions.CoAP.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30204.135 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C2499C75-EDBE-4552-B855-52E2B12BC4E4}" ProjectSection(SolutionItems) = preProject @@ -21,6 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Example.Cl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Example.Server", "WorldDirect.CoAP.Example.Server\WorldDirect.CoAP.Example.Server.csproj", "{3EF4EA32-6F5B-4B2E-83BD-08D670D5A361}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.DTLS", "WorldDirect.CoAP.DTLS\WorldDirect.CoAP.DTLS.csproj", "{22366801-AB3F-4F99-AD26-80B6755F0709}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Hosting", "WorldDirect.CoAP.Hosting\WorldDirect.CoAP.Hosting.csproj", "{7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAPS.DTLS.Specs", "WorldDirect.CoAPS.DTLS.Specs\WorldDirect.CoAPS.DTLS.Specs.csproj", "{94ACFE1B-59C3-4E3F-BBB2-1682D440F103}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +85,54 @@ Global {3EF4EA32-6F5B-4B2E-83BD-08D670D5A361}.Release|x64.Build.0 = Release|Any CPU {3EF4EA32-6F5B-4B2E-83BD-08D670D5A361}.Release|x86.ActiveCfg = Release|Any CPU {3EF4EA32-6F5B-4B2E-83BD-08D670D5A361}.Release|x86.Build.0 = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|x64.ActiveCfg = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|x64.Build.0 = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|x86.ActiveCfg = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Debug|x86.Build.0 = Debug|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|Any CPU.Build.0 = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|x64.ActiveCfg = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|x64.Build.0 = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|x86.ActiveCfg = Release|Any CPU + {22366801-AB3F-4F99-AD26-80B6755F0709}.Release|x86.Build.0 = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|x64.Build.0 = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Debug|x86.Build.0 = Debug|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|Any CPU.Build.0 = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|x64.ActiveCfg = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|x64.Build.0 = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|x86.ActiveCfg = Release|Any CPU + {7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}.Release|x86.Build.0 = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|x64.Build.0 = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Debug|x86.Build.0 = Debug|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|x64.ActiveCfg = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|x64.Build.0 = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|x86.ActiveCfg = Release|Any CPU + {C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}.Release|x86.Build.0 = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|x64.ActiveCfg = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|x64.Build.0 = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|x86.ActiveCfg = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Debug|x86.Build.0 = Debug|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|Any CPU.Build.0 = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|x64.ActiveCfg = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|x64.Build.0 = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|x86.ActiveCfg = Release|Any CPU + {94ACFE1B-59C3-4E3F-BBB2-1682D440F103}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs new file mode 100644 index 0000000..ccbdcdd --- /dev/null +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2011-2015, Longxiang He , + * SmeshLink Technology Co. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY. + * + * This file is part of the CoAP.NET, a CoAP framework in C#. + * Please see README for more information. + */ + +namespace WorldDirect.CoAP.Net +{ + using System; + using System.Net; + using System.Runtime.CompilerServices; + using System.Runtime.Serialization; + using System.Threading; + using Channel; + using Codec; + using DTLS; + using Log; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging; + using Stack; + using Threading; + + /// + /// EndPoint encapsulates the dtlsStack that executes the CoAP protocol. + /// + public partial class CoAPSEndpoint : IEndPoint, IOutbox + { + + readonly ICoapConfig _config; + readonly CoapStack _coapStack; + private IMessageDeliverer _deliverer; + private IMatcher _matcher; + private Int32 _running; + private IExecutor _executor; + private DTLSChannel channel; + private ILogger log = LogManager.GetLogger(); + + /// + public string Scheme => CoapConstants.SecureUriScheme; + + /// + public event EventHandler> SendingRequest; + /// + public event EventHandler> SendingResponse; + /// + public event EventHandler> SendingEmptyMessage; + /// + public event EventHandler> ReceivingRequest; + /// + public event EventHandler> ReceivingResponse; + /// + public event EventHandler> ReceivingEmptyMessage; + + /// + /// Instantiates a new endpoint with the + /// specified channel and configuration. + /// + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) + { + _config = config; + _matcher = new Matcher(this._config); + _coapStack = new CoapStack(this._config); + UDPChannel channel = new UDPChannel(new IPEndPoint(IPAddress.Any, 5684)); + channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; + this.channel = new DTLSChannel(channel, cache, dtlsConfig); + this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; + } + + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, UDPChannel channel, ICoapConfig config) + { + _config = config; + _matcher = new Matcher(this._config); + _coapStack = new CoapStack(this._config); + this.channel = new DTLSChannel(channel, cache, dtlsConfig); + this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; + } + + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, IPEndPoint endpoint, ICoapConfig config) + { + _config = config; + _matcher = new Matcher(this._config); + _coapStack = new CoapStack(this._config); + var udpChannel = new UDPChannel(endpoint); + udpChannel.ReceivePacketSize = this._config.ChannelReceivePacketSize; + this.channel = new DTLSChannel(udpChannel, cache, dtlsConfig); + this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; + } + + + public DTLSServerConfig DTLSConfig { get; } + + /// + public ICoapConfig Config + { + get { return _config; } + } + + public IExecutor Executor + { + get { return _executor; } + set + { + _executor = value ?? Executors.NoThreading; + _coapStack.Executor = _executor; + } + } + + /// + public System.Net.EndPoint LocalEndPoint => this.channel.LocalEndPoint; + + /// + public IMessageDeliverer MessageDeliverer + { + set { _deliverer = value; } + get + { + if (_deliverer == null) + _deliverer = new ClientMessageDeliverer(); + return _deliverer; + } + } + + /// + public IOutbox Outbox + { + get { return this; } + } + + /// + public Boolean Running + { + get { return _running > 0; } + } + + /// + public void Start() + { + if (System.Threading.Interlocked.CompareExchange(ref _running, 1, 0) > 0) + return; + + if (_executor == null) + Executor = Executors.Default; + + try + { + _matcher.Start(); + this.channel.Start(); + } + catch + { + log?.LogWarning("Cannot start secure endpoint at " + this.channel.LocalEndPoint); + Stop(); + throw; + } + log?.LogDebug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); + } + + /// + public void Stop() + { + if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) + return; + + log?.LogDebug("Stopping secure endpoint bound to " + this.LocalEndPoint); + this.channel.Stop(); + _matcher.Stop(); + _matcher.Clear(); + } + + /// + public void Clear() + { + _matcher.Clear(); + } + + /// + public void Dispose() + { + if (Running) + Stop(); + IDisposable d = _matcher as IDisposable; + if (d != null) + d.Dispose(); + } + + /// + public void SendRequest(Request request) + { + _executor.Start(() => _coapStack.SendRequest(request)); + } + + /// + public void SendResponse(Exchange exchange, Response response) + { + _executor.Start(() => _coapStack.SendResponse(exchange, response)); + } + + /// + public void SendEmptyMessage(Exchange exchange, EmptyMessage message) + { + _executor.Start(() => _coapStack.SendEmptyMessage(exchange, message)); + } + + private void ReceiveRequest(Exchange exchange, Request request) + { + _executor.Start(() => _coapStack.ReceiveRequest(exchange, request)); + } + + private void ReceiveResponse(Exchange exchange, Response response) + { + _executor.Start(() => _coapStack.ReceiveResponse(exchange, response)); + } + + + private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) + { + IMessageDecoder decoder = Spec.NewMessageDecoder(e.Data); + if (decoder.IsRequest) + { + Request request; + try + { + request = decoder.DecodeRequest(); + request.EndPoint = this; + } + catch (Exception) + { + if (decoder.IsReply) + { + log?.LogWarning("Message format error caused by " + e.EndPoint); + } + else + { + // manually build RST from raw information + EmptyMessage rst = new EmptyMessage(MessageType.RST); + rst.Destination = e.EndPoint; + rst.ID = decoder.ID; + + Fire(SendingEmptyMessage, rst); + this.channel.Send(Serialize(rst), e.EndPoint); + + log?.LogWarning("Message format error caused by " + e.EndPoint + " and reseted."); + } + return; + } + + request.Source = e.EndPoint; + + using var scope = this.log.BeginScope(new List> + { + new ("MessageId", request.ID), + new ("Remote", request.Source), + new ("CoAPResource", request.URI.AbsolutePath), + }); + + Fire(ReceivingRequest, request); + + if (!request.IsCancelled) + { + Exchange exchange = _matcher.ReceiveRequest(request); + if (exchange != null) + { + exchange.EndPoint = this; + exchange.Set(nameof(DTLSClientAuthentication), e.ClientAuthentication); + this.ReceiveRequest(exchange, request); + } + } + } + else if (decoder.IsResponse) + { + Response response = decoder.DecodeResponse(); + response.Source = e.EndPoint; + + using var scope = this.log.BeginScope(new List> + { + new ("MessageId", response.ID), + new ("Remote", response.Source), + }); + + Fire(ReceivingResponse, response); + + if (!response.IsCancelled) + { + Exchange exchange = _matcher.ReceiveResponse(response); + if (exchange != null) + { + using var responseScope = this.log.BeginScope(new List> + { + new ("CoAPResource", exchange.Request.URI.AbsolutePath), + }); + response.RTT = (DateTime.Now - exchange.Timestamp).TotalMilliseconds; + exchange.EndPoint = this; + this.ReceiveResponse(exchange, response); + } + else if (response.Type != MessageType.ACK) + { + log?.LogDebug("Rejecting unmatchable response from " + e.EndPoint); + Reject(response); + } + } + } + else if (decoder.IsEmpty) + { + EmptyMessage message = decoder.DecodeEmptyMessage(); + message.Source = e.EndPoint; + + Fire(ReceivingEmptyMessage, message); + + if (!message.IsCancelled) + { + // CoAP Ping + if (message.Type == MessageType.CON || message.Type == MessageType.NON) + { + log?.LogDebug("Responding to ping by " + e.EndPoint); + Reject(message); + } + else + { + Exchange exchange = _matcher.ReceiveEmptyMessage(message); + if (exchange != null) + { + exchange.EndPoint = this; + _coapStack.ReceiveEmptyMessage(exchange, message); + } + } + } + } + else + { + log?.LogDebug("Silently ignoring non-CoAP message from " + e.EndPoint); + } + } + + private void Reject(Message message) + { + EmptyMessage rst = EmptyMessage.NewRST(message); + + Fire(SendingEmptyMessage, rst); + + if (!rst.IsCancelled) + this.channel.Send(Serialize(rst), rst.Destination); + } + + private Byte[] Serialize(EmptyMessage message) + { + Byte[] bytes = message.Bytes; + if (bytes == null) + { + bytes = Spec.NewMessageEncoder().Encode(message); + message.Bytes = bytes; + } + return bytes; + } + + private Byte[] Serialize(Request request) + { + Byte[] bytes = request.Bytes; + if (bytes == null) + { + bytes = Spec.NewMessageEncoder().Encode(request); + request.Bytes = bytes; + } + return bytes; + } + + private Byte[] Serialize(Response response) + { + Byte[] bytes = response.Bytes; + if (bytes == null) + { + bytes = Spec.NewMessageEncoder().Encode(response); + response.Bytes = bytes; + } + return bytes; + } + + private void Fire(EventHandler> handler, T msg) where T : Message + { + if (handler != null) + handler(this, new MessageEventArgs(msg)); + } + + void IOutbox.SendRequest(Exchange exchange, Request request) + { + _matcher.SendRequest(exchange, request); + + Fire(SendingRequest, request); + + if (!request.IsCancelled) + this.channel.Send(Serialize(request), request.Destination); + } + + void IOutbox.SendResponse(Exchange exchange, Response response) + { + _matcher.SendResponse(exchange, response); + + Fire(SendingResponse, response); + + if (!response.IsCancelled) + this.channel.Send(Serialize(response), response.Destination); + } + + void IOutbox.SendEmptyMessage(Exchange exchange, EmptyMessage message) + { + _matcher.SendEmptyMessage(exchange, message); + + Fire(SendingEmptyMessage, message); + + if (!message.IsCancelled) + this.channel.Send(Serialize(message), message.Destination); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLS12KeyFileData.cs b/WorldDirect.CoAP.DTLS/DTLS12KeyFileData.cs new file mode 100644 index 0000000..de87eb5 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLS12KeyFileData.cs @@ -0,0 +1,65 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Reflection; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; + +/// +/// Represents the necessary information to write a ssl keylog file for (D)TLS 1.2 to decrypt communication. +/// +public struct DTLS12KeyFileData +{ + /// + /// Create the data from a . + /// + /// Helper because data of a tls secret cant be extracted easily. + /// The client random of the clients first handshake message. + /// The pre master secret. + /// The created data if it was possible or null on failure. + public static DTLS12KeyFileData? FromSecret(byte[]? clientRandom, TlsSecret? secret) + { + if (clientRandom == null || secret == null) + { + return null; + } + + // cant use extract of MasterSecret -> the secret would be lost. + if (secret.GetType() == typeof(BcTlsSecret)) + { + var fieldInfo = typeof(BcTlsSecret).GetField("m_data", BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo == null) + { + return null; + } + var secretData = (byte[]?)fieldInfo.GetValue(secret); + if (secretData == null) + { + return null; + } + return new DTLS12KeyFileData(clientRandom, secretData); + } + + return null; + } + + /// + /// Initializes a new instance of the class. + /// + /// The client random of the clients first handshake message. + /// The pre master secret. + public DTLS12KeyFileData(byte[] clientRandom, byte[] preMasterSecret) + { + this.ClientRandom = clientRandom; + this.PreMasterSecret = preMasterSecret; + } + + /// + /// Gets or sets the client random of the first handshake message. + /// + public byte[] ClientRandom { get; set; } + + /// + /// Gets or sets the pre master secret. + /// + public byte[] PreMasterSecret { get; set; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs new file mode 100644 index 0000000..a50a729 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -0,0 +1,106 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System.Net; + using Channel; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging; + using WorldDirect.CoAP.Log; + + /// + /// Represents the dtls channel for a coap communication. + /// + public class DTLSChannel : IChannel + { + private readonly UDPChannel channel; + private readonly DTLSSessionManager sessionManager; + private readonly ILogger logger = LogManager.GetLogger(); + + /// + /// Initializes a new instance of the class. + /// + /// The underlying udp channel used to send/receive data. + /// The cache to store dtls sessions. + /// The configuration of the dtls server. + /// The timeout after which a session is deleted. + public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtlsConfig, TimeSpan sessionTimeout) + { + this.channel = channel; + this.channel.DataReceived += DtlsReceived; + var config = new DTLSSessionConfig() { MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = sessionTimeout, HandshakeTimeout = dtlsConfig.HandshakeTimeout,}; + this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), dtlsConfig, config); + } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying udp channel used to send/receive data. + /// The cache to store dtls sessions. + /// The configuration of the dtls server. + public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtlsConfig) + : this(channel, cache, dtlsConfig, TimeSpan.FromMinutes(2)) + { + } + + private void DtlsReceived(object? sender, DataReceivedEventArgs e) + { + Task.Factory.StartNew(() => this.sessionManager.ReceivedUdpPacket(e.Data, e.EndPoint)).ConfigureAwait(false); + } + + /// + public void Dispose() + { + this.Stop(); + } + + /// + public EndPoint LocalEndPoint => this.channel.LocalEndPoint; + + /// + public event EventHandler? DataReceived; + + /// + /// An event to forward dtls relevant data with a received message. + /// + public event EventHandler? DtlsDataReceived; + + /// + public void Start() + { + this.channel.Start(); + this.sessionManager.DataReceived += DecryptedForwarding; + } + + /// + public void Stop() + { + this.channel.Stop(); + this.sessionManager.Stop(); + } + + /// + public void Send(byte[] data, EndPoint ep) + { + try + { + this.logger.LogTrace("Sending {Bytes} decrypted bytes to {Remote}", data.Length, ep); + this.sessionManager.SendTo(data, ep); + } + catch (Exception e) + { + this.logger.LogError(e, "Could not send data to {Remote}", ep); + } + } + + private void DecryptedForwarding(object? sender, DTLSDataReceivedEventArgs e) + { + this.logger.LogTrace("Received {Bytes} decrypted bytes from {Remote}", e.Data.Length, e.EndPoint); + this.DataReceived?.Invoke(this, e); + this.DtlsDataReceived?.Invoke(this, e); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSClientAuthentication.cs b/WorldDirect.CoAP.DTLS/DTLSClientAuthentication.cs new file mode 100644 index 0000000..e72d38f --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSClientAuthentication.cs @@ -0,0 +1,40 @@ +namespace WorldDirect.CoAP.Net; + +using System.Security.Cryptography.X509Certificates; + +public class DTLSClientAuthentication +{ + private DTLSClientAuthentication(X509Certificate? certificate, string? pskIdentity) + { + this.Certificate = certificate; + this.PskIdentity = pskIdentity; + } + + /// + /// Initializes a new instance of the class. + /// + /// The certificate of the peer. + public DTLSClientAuthentication(X509Certificate certificate) + : this(certificate, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The public identity of the peer. + public DTLSClientAuthentication(string pskIdentity) + : this(null, pskIdentity) + { + } + + /// + /// Gets the certificate of the client. + /// + public X509Certificate? Certificate { get; } + + /// + /// Gets the public identity of the client to identify the psk. + /// + public string? PskIdentity { get; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSDataReceivedEventArgs.cs b/WorldDirect.CoAP.DTLS/DTLSDataReceivedEventArgs.cs new file mode 100644 index 0000000..7b8d656 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSDataReceivedEventArgs.cs @@ -0,0 +1,50 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Channel; +using Net; + +public class DTLSDataReceivedEventArgs : DataReceivedEventArgs +{ + private DTLSDataReceivedEventArgs(byte[] data, EndPoint endPoint, X509Certificate? certificate, string? pskIdentity) + : base(data, endPoint) + { + if (certificate != null) + { + this.ClientAuthentication = new DTLSClientAuthentication(certificate); + } + else if (pskIdentity != null) + { + this.ClientAuthentication = new DTLSClientAuthentication(pskIdentity); + } + else + { + throw new ArgumentException("Unauthenticated communication is not allowed"); + } + } + + /// + /// Initialize a new instance of the class with the clients certificate. + /// + /// The received payload. + /// The endpoint of the peer. + /// The certificate of the peer. + public DTLSDataReceivedEventArgs(byte[] data, EndPoint endPoint, X509Certificate certificate) + : this(data, endPoint, certificate, null) + { + } + + /// + /// Initialize a new instance of the class with the clients certificate. + /// + /// The received payload. + /// The endpoint of the peer. + /// The public identity of the peer. + public DTLSDataReceivedEventArgs(byte[] data, EndPoint endPoint, string pskIdentity) + : this(data, endPoint, null, pskIdentity) + { + } + + public DTLSClientAuthentication ClientAuthentication { get; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSMetrics.cs b/WorldDirect.CoAP.DTLS/DTLSMetrics.cs new file mode 100644 index 0000000..dd7f58f --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSMetrics.cs @@ -0,0 +1,77 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Tracing; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + [EventSource(Name = "WorldDirect.CoAP.DTLS")] + public sealed class DTLSMetrics : EventSource + { + /// + /// The provider to collect DTLS metrics. + /// + public static readonly DTLSMetrics Log = new (); + + private PollingCounter? activeSessionsCounter; + private PollingCounter? failedHandshakesCounter; + + private long activeSessions; + private long failedHandshakes; + + private DTLSMetrics() + { + + } + + public void SessionAdded() + { + Interlocked.Increment(ref this.activeSessions); + } + + public void SessionRemoved() + { + Interlocked.Decrement(ref this.activeSessions); + } + + public void HandshakeFailed() + { + Interlocked.Increment(ref this.failedHandshakes); + } + + /// + /// Releases the unmanaged resources used by the class and optionally releases the managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + this.activeSessionsCounter?.Dispose(); + this.activeSessionsCounter = null; + + this.failedHandshakesCounter?.Dispose(); + this.failedHandshakesCounter = null; + } + + /// + /// Called when the current event source is updated by the controller. + /// + /// The arguments for the event. + protected override void OnEventCommand(EventCommandEventArgs command) + { + if (command.Command == EventCommand.Enable) + { + this.activeSessionsCounter ??= new PollingCounter("dtls-active-sessions", this, () => Volatile.Read(ref this.activeSessions)) + { + DisplayName = "Active DTLS Sessions" + }; + + this.failedHandshakesCounter ??= new PollingCounter("dtls-failed-handshakes", this, () => Volatile.Read(ref this.failedHandshakes)) + { + DisplayName = "Failed DTLS Handshakes" + }; + } + } + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs new file mode 100644 index 0000000..232f612 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -0,0 +1,233 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Runtime.CompilerServices; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; + +/// +/// Represents a dtls server implementation which communicates with one client. +/// +public class DTLSServer : AbstractTlsServer +{ + private readonly DTLSServerConfig config; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration of the server. + public DTLSServer(DTLSServerConfig config) : base(config.Crypto) + { + this.config = config; + this.IsAuthenticated = false; + } + + /// + /// Gets whether the client connected with this server is authenticated. + /// + public bool IsAuthenticated { get; private set; } + + /// + /// Gets the certificate of the connected client. + /// + public TlsCertificate? PeerCertificate + { + get + { + if (this.m_context.SecurityParameters.PeerCertificate == null) + { + return null; + } + return this.m_context.SecurityParameters.PeerCertificate.IsEmpty ? null : this.m_context.SecurityParameters.PeerCertificate.GetCertificateAt(0); + } + } + + /// + /// Gets the used PSK identity of the remote. + /// + public byte[] PskIdentity { get; private set; } = Array.Empty(); + + /// + /// Get the maximum size of a dtls the remote supports. + /// + public int? MaxFragmentLength + { + get + { + if (this.m_context.SecurityParameters.MaxFragmentLength == 1) + { + return 512; + } + if (this.m_context.SecurityParameters.MaxFragmentLength == 2) + { + return 1024; + } + if (this.m_context.SecurityParameters.MaxFragmentLength == 3) + { + return 2048; + } + if (this.m_context.SecurityParameters.MaxFragmentLength == 4) + { + return 4096; + } + + return null; + } + } + + /// + /// Get the timeout of handshake. + /// + /// The timeout in milliseconds. + public override int GetHandshakeTimeoutMillis() + { + return (int)this.config.HandshakeTimeout.TotalMilliseconds; + } + + /// + /// Get the supported TLS versions. + /// + /// The supported TLS versions. + protected override ProtocolVersion[] GetSupportedVersions() + { + return ProtocolVersion.DTLSv12.Only(); + } + + /// + /// Get all supported cipher suites. + /// + /// The supported cipher suites. + protected override int[] GetSupportedCipherSuites() + { + return this.config.CipherSuites.ToArray(); + } + + public override void NotifyHandshakeComplete() + { + if (this.m_context.SecurityParameters.PskIdentity != null) + { + this.PskIdentity = this.m_context.SecurityParameters.PskIdentity; + this.IsAuthenticated = true; + } + if (this.config.KeyStore != null) + { + var keyData = DTLS12KeyFileData.FromSecret(this.m_context.SecurityParameters.ClientRandom, this.m_context.SecurityParameters.MasterSecret); + if (keyData != null) + { + this.config.KeyStore.Store(keyData.Value); + } + } + } + + public override TlsPskIdentityManager GetPskIdentityManager() + { + if (this.config.PskManager == null) + { + return null; + } + return this.config.PskManager; + } + + /// + /// Get the certificate request send to the client. + /// + /// The generated request. + public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() + { + // if no CAs are registered, we wont need a certificate for authentication. + if (!this.config.CAs.Any()) + { + return null; + } + + var serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); + // currently only ecdsa supported + // todo check if any is RSA certificate and add RSA certificate type + serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); + + // send back a list of supported CAs + + var authorities = this.config.CAs.Select(c => c.SubjectDN).ToList(); + + short[] certificateTypes = new short[] { ClientCertificateType.ecdsa_sign, }; + + return new CertificateRequest(certificateTypes, serverSigAlgs, authorities); + } + + /// + /// Get the credentials of the server. + /// + /// The credentials. + /// Thrown when a key exchange algorithm is not supported. + public override TlsCredentials GetCredentials() + { + int keyExchangeAlgorithm = m_context.SecurityParameters.KeyExchangeAlgorithm; + switch (keyExchangeAlgorithm) + { + case KeyExchangeAlgorithm.PSK: + return null; + case KeyExchangeAlgorithm.ECDHE_ECDSA: + return GetECDsaSignerCredentials(); + default: + throw new TlsFatalAlert(AlertDescription.handshake_failure, "Unsupported exchange algorithm"); + } + } + + /// + /// Handling of the reported client certificate. + /// + /// The certificate of the client. + public override void NotifyClientCertificate(Certificate clientCertificate) + { + if (clientCertificate.IsEmpty) + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + + var chain = clientCertificate.GetCertificateList()!; + var chainAsCertificate = chain.Select(c => new X509Certificate(c.GetEncoded())).ToArray(); + var trustAnchors = this.config.CAs.Select(c => new TrustAnchor(c, null)).ToList(); + + var parameters = new PkixParameters(new SortedSet(trustAnchors)); + parameters.IsRevocationEnabled = false; + var path = new PkixCertPath(chainAsCertificate); + var validator = new PkixCertPathValidator(); + validator.Validate(path, parameters); + this.IsAuthenticated = true; + } + + private TlsCredentialedSigner GetECDsaSignerCredentials() + { + var clientSupportedSigAlgs = this.m_context.SecurityParameters.ClientSigAlgs; + var clientECDsaSigAlgs = clientSupportedSigAlgs.Where(sig => sig.Signature == SignatureAlgorithm.ecdsa); + if (!clientECDsaSigAlgs.Any()) + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + + // the servername the client wants to connect + var serverNames = this.m_context.SecurityParameters.ClientServerNames; + + var ecCert = FindCertificate(serverNames); + + var signer = new BcTlsECDsaSigner((BcTlsCrypto)this.Crypto, ecCert.PrivateKey); + var parameter = new TlsCryptoParameters(this.m_context); + var ecdsa = SignatureAlgorithm.ecdsa; + var alg = clientSupportedSigAlgs.First(alg => alg.Hash == this.m_context.SecurityParameters.PrfCryptoHashAlgorithm && alg.Signature == ecdsa); + var credSigner = new DefaultTlsCredentialedSigner(parameter, signer, ecCert.Certificate, alg); + return credSigner; + } + + private EcServerCertificate FindCertificate(IList names) + { + // todo implement if multiple certificates are required. + // how to distinguish? + // https://en.wikipedia.org/wiki/Subject_Alternative_Name#:~:text=Subject%20Alternative%20Name%20(SAN)%20is,Subject%20Alternative%20Names%20(SANs). + // or only common name? + return this.config.EcCertificate!; + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs new file mode 100644 index 0000000..30b1b01 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -0,0 +1,67 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; + +/// +/// Represents the configuration of the . +/// +public class DTLSServerConfig +{ + /// + /// Initializes a new instance of the class. + /// + /// The configuration to copy. + public DTLSServerConfig(MutableDTLSServerConfig config) + { + this.Crypto = config.Crypto; + this.EcCertificate = config.EcCertificate; + this.CAs = config.CAs; + this.CipherSuites = config.CipherSuites; + this.HandshakeTimeout = config.HandshakeTimeout; + this.PskManager = config.PskManager; + this.KeyStore = config.KeyStore; + } + + /// + /// Gets or sets the crypto stack. + /// + public BcTlsCrypto Crypto { get; } + + /// + /// Gets or sets the certificate of the server. + /// + public EcServerCertificate? EcCertificate { get; } + + /// + /// Gets or sets the CA to authorize the connecting clients. + /// + public List CAs { get; } = new List(); + + /// + /// Gets or sets the available cipher suites. + /// + public List CipherSuites { get; } = new(); + + /// + /// Gets or sets the timeout of the dtls handshake. + /// + /// + /// 0 means no timeout + /// + public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; + + /// + /// Gets or sets the provider for psk keys. + /// + public TlsPskIdentityManager? PskManager { get; } + + /// + /// Gets the store where the session keys should be stored. + /// + /// + /// !!! ATTENTION !!! Will export session keys of communication! Only use in DEV environment. + /// + public IKeyStore? KeyStore { get; } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs new file mode 100644 index 0000000..cf20bfe --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -0,0 +1,166 @@ +namespace WorldDirect.CoAP.DTLS; + +using System; +using System.Data; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Channel; +using Microsoft.Extensions.Logging; +using Org.BouncyCastle.Tls; +using WorldDirect.CoAP.Log; +using WorldDirect.CoAP.Net; + +internal class DTLSSession : IDisposable +{ + private readonly DTLSSessionConfig config; + private readonly UdpTransport transport; + private readonly DtlsServerProtocol protocol; + private readonly DTLSServer dtlsServer; + private DtlsTransport? dtlsTransport; + private bool HandshakeFailed = false; + private readonly ILogger logger; + + public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, DTLSSessionConfig config) + { + this.config = config; + this.transport = new UdpTransport(sender, remote, config.MaxPacketLength, config.HandshakeTimeout); + this.protocol = new DtlsServerProtocol(); + this.dtlsServer = server; + this.logger = LogManager.GetLogger(); + } + + public EndPoint Remote => this.transport.Remote; + + /// + /// An event when a new decrypted payload was received. + /// + public event EventHandler? DataReceived; + + /// + /// An event when the handshake was finished. + /// + public event EventHandler? HandshakeFinished; + + /// + /// Start the task which handles the received data. + /// + public void Start() + { + // perform handshake asynchronously, would be blocking otherwise + Task.Factory.StartNew(this.HandleSession, TaskCreationOptions.LongRunning).ConfigureAwait(false); + } + + /// + /// Send the specified plaintext payload encrypted with this session. + /// + /// The plaintext payload. + public void Send(ReadOnlySpan payload) + { + lock (this.dtlsTransport!) + { + if (payload.Length > this.dtlsServer.MaxFragmentLength) + { + this.logger.LogWarning("Cant send message with {Bytes} bytes to {Remote} because buffer of remote is to small.", payload.Length, this.Remote); + return; + } + this.dtlsTransport?.Send(payload); + } + } + + /// + /// Enqueue a received dtls message for this session. + /// + /// The dtls message. + public void Enqueue(ReadOnlySpan payload) + { + if (this.HandshakeFailed) + { + return; + } + this.transport.Enqueue(payload); + + // if handshake was was performed successfully, decrypt data directly + if (dtlsTransport != null) + { + lock (this.dtlsTransport) + { + var rxBuffer = new byte[this.config.MaxPacketLength]; + int length; + try + { + do + { + length = this.dtlsTransport!.Receive(rxBuffer, 1); + if (length > 0) + { + this.InvokeDataReceived(rxBuffer.Take(length).ToArray()); + } + } while (length > 0); + } + catch (Exception ex) + { + this.logger.LogTrace(ex, "Cant receive {Bytes} decrypted bytes from {Remote}", payload.Length, this.Remote); + } + } + } + else + { + this.logger.LogTrace("Enqueued {Bytes} for decrypting. Skipped decrypting as handshake with {Remote} is not finished", payload.Length, this.Remote); + } + } + + private void InvokeDataReceived(byte[] payload) + { + if (this.dtlsServer.IsAuthenticated) + { + if (this.dtlsServer.PeerCertificate != null) + { + var peerCert = new X509Certificate(this.dtlsServer.PeerCertificate.GetEncoded()); + this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(payload, this.transport.Remote, peerCert)); + } + else if (this.dtlsServer.PskIdentity.Any()) + { + this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(payload, this.transport.Remote, Encoding.ASCII.GetString(this.dtlsServer.PskIdentity))); + } + } + else + { + throw new NotImplementedException($"Unauthenticated communication is not implemented"); + } + } + + private void HandleSession() + { + try + { + // perform handshake + this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); + this.logger.LogInformation("Finished handshake with {Remote} successfully", this.Remote); + } + catch (TlsTimeoutException e) + { + this.HandshakeFailed = true; + this.logger.LogError(e, "{Remote} failed handshake because of timeout ({Timeout})", this.Remote, this.config.HandshakeTimeout); + } + catch (TlsFatalAlert e) + { + this.logger.LogError(e, "{Remote} failed handshake", this.Remote); + this.HandshakeFailed = true; + } + catch (Exception e) + { + this.logger.LogError(e, "{Remote} failed handshake", this.Remote); + this.HandshakeFailed = true; + } + finally + { + this.HandshakeFinished?.Invoke(this, new HandshakeFinishedEventArgs() { Successful = !this.HandshakeFailed }); + } + } + + public void Dispose() + { + this.transport.Dispose(); + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs new file mode 100644 index 0000000..72d133d --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs @@ -0,0 +1,22 @@ +namespace WorldDirect.CoAP.DTLS; + +/// +/// Represents the configuration of a dtls session. +/// +public class DTLSSessionConfig +{ + /// + /// Gets or sets the timeout of a session. + /// + public TimeSpan SessionTimeout { get; set; } + + /// + /// Gets or sets the maximum packet length of a udp payload. + /// + public int MaxPacketLength { get; set; } + + /// + /// Gets or sets the maximum duration of a handshake. + /// + public TimeSpan HandshakeTimeout { get; set; } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs new file mode 100644 index 0000000..73f8bca --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -0,0 +1,151 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Net; + using System.Text; + using Channel; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Internal; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using Org.BouncyCastle.Asn1.Nist; + using Org.BouncyCastle.Asn1.X509; + using Org.BouncyCastle.Tls; + using Org.BouncyCastle.Tls.Crypto.Impl.BC; + using WorldDirect.CoAP.Log; + + /// + /// Handles all dtls related traffic. + /// + public class DTLSSessionManager + { + private readonly IMemoryCache cache; + private readonly IUDPSender sender; + private readonly DTLSServerConfig dtlsServerConfig; + private readonly DTLSSessionConfig config; + private readonly ILogger log = LogManager.GetLogger(); + + /// + /// Initializes a new instance of the class. + /// + /// A cache to store the sessions. + /// An object to send udp packets. + /// The configuration of the dtls server. + /// The configuration for the sessions. + public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfig dtlsServerConfig, DTLSSessionConfig config) + { + this.cache = cache; + this.sender = sender; + this.dtlsServerConfig = dtlsServerConfig; + this.config = config; + this.log.LogDebug("Configured DTLS handshake timeout: {HandshakeTimeout}", dtlsServerConfig.HandshakeTimeout); + } + + /// + /// An event to notify listener a new decrypted udp packet was received. + /// + public event EventHandler? DataReceived; + + /// + /// Send a udp packet encrypted to the remote endpoint. + /// + /// The packet to encrypt and send. + /// The remote endpoint. + public void SendTo(ReadOnlySpan packet, EndPoint endPoint) + { + // cache.TryGetValue does not work (always returns false with null object...) + try + { + var session = this.cache.Get(GetKey(endPoint)); + if (session != null) + { + session.Send(packet); + return; + } + } + catch (ObjectDisposedException) + { + // might happen because eviction happens after getting object but before sending is called + } + this.log.LogWarning("Tried to send data to {Remote} but no session available", endPoint); + } + + /// + /// Stops the manager. + /// + public void Stop() + { + + } + + /// + /// A udp packet was received for a session. + /// + /// The received packet. + /// The endpoint who sent the packet. + internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) + { + var data = packet.ToArray(); + short recordType = TlsUtilities.ReadUint8(data, 0); + int epoch = TlsUtilities.ReadUint16(data, 3); + short handshakeType = TlsUtilities.ReadUint8(data, 13); + DTLSSession? session = null; + + session = this.cache.Get(GetKey(endPoint)); + + if (session == null && (recordType == ContentType.handshake) && (epoch == 0) && (handshakeType == HandshakeType.client_hello)) + { + session = this.cache.GetOrCreate(GetKey(endPoint), entry => + { + entry.SlidingExpiration = config.SessionTimeout; + var callback = new PostEvictionCallbackRegistration() {EvictionCallback = OnEviction, State = this}; + entry.PostEvictionCallbacks.Add(callback); + entry.Priority = CacheItemPriority.NeverRemove; + + var s = new DTLSSession(this.sender, new DTLSServer(this.dtlsServerConfig), endPoint, this.config); + s.DataReceived += DecryptedReceived; + s.HandshakeFinished += HandshakeFinished; + this.log.LogDebug("Start DTLS connection with {Remote}", endPoint); + s.Start(); + DTLSMetrics.Log.SessionAdded(); + return s; + }); + } + + if (session != null) + { + this.log.LogTrace("Received {Bytes} encrypted Bytes from {Remote}", packet.Length, endPoint); + session.Enqueue(packet); + } + } + + private void HandshakeFinished(object? sender, HandshakeFinishedEventArgs e) + { + var session = (sender as DTLSSession)!; + if (!e.Successful) + { + DTLSMetrics.Log.HandshakeFailed(); + this.cache.Remove(session.Remote.ToString()); + } + } + + private void DecryptedReceived(object? _, DTLSDataReceivedEventArgs e) + { + this.DataReceived?.Invoke(this, e); + } + + private static string GetKey(EndPoint remote) + { + return $"dtlssession_{remote}"; + } + + private static void OnEviction(object key, object value, EvictionReason reason, object state) + { + var manager = (DTLSSessionManager)state; + var obj = value as DTLSSession; + obj.Dispose(); + manager.log.LogDebug("Session with {Remote} timed out", obj.Remote); + DTLSMetrics.Log.SessionRemoved(); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/EcServerCertificate.cs b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs new file mode 100644 index 0000000..92460c4 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs @@ -0,0 +1,31 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Tls; + +/// +/// Represents a elliptic curve certificate with its private key. +/// +public class EcServerCertificate +{ + /// + /// Initializes a new instance of the class. + /// + /// The server certificate. + /// The corresponding private key. + public EcServerCertificate(Certificate certificate, ECPrivateKeyParameters privateKey) + { + this.Certificate = certificate; + this.PrivateKey = privateKey; + } + + /// + /// Gets the certificate. + /// + public Certificate Certificate { get; } + + /// + /// Gets the private key. + /// + public ECPrivateKeyParameters PrivateKey { get; } +} diff --git a/WorldDirect.CoAP.DTLS/HandshakeFinishedEventArgs.cs b/WorldDirect.CoAP.DTLS/HandshakeFinishedEventArgs.cs new file mode 100644 index 0000000..cc46d44 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/HandshakeFinishedEventArgs.cs @@ -0,0 +1,12 @@ +namespace WorldDirect.CoAP.DTLS; + +/// +/// Represents event args when a handshake is completed. +/// +internal class HandshakeFinishedEventArgs : EventArgs +{ + /// + /// Gets or sets a flag indicating the success of a handshake. + /// + public bool Successful { get; set; } +} diff --git a/WorldDirect.CoAP.DTLS/IKeyStore.cs b/WorldDirect.CoAP.DTLS/IKeyStore.cs new file mode 100644 index 0000000..3d4267d --- /dev/null +++ b/WorldDirect.CoAP.DTLS/IKeyStore.cs @@ -0,0 +1,20 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + /// + /// Provides an interface to store session keys. + /// + public interface IKeyStore + { + /// + /// Store the data. + /// + /// The data to write. + void Store(DTLS12KeyFileData data); + } +} diff --git a/WorldDirect.CoAP.DTLS/IUDPSender.cs b/WorldDirect.CoAP.DTLS/IUDPSender.cs new file mode 100644 index 0000000..bde2d12 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/IUDPSender.cs @@ -0,0 +1,16 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; + +/// +/// An interface to send UDP data. +/// +public interface IUDPSender +{ + /// + /// Send a message to the remote. + /// + /// The message to send. + /// The remote. + void SendTo(ReadOnlySpan payload, EndPoint remote); +} diff --git a/WorldDirect.CoAP.DTLS/MutableDTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/MutableDTLSServerConfig.cs new file mode 100644 index 0000000..9374148 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/MutableDTLSServerConfig.cs @@ -0,0 +1,52 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; + +/// +/// Represents the configuration of the in build mode. +/// +public class MutableDTLSServerConfig +{ + /// + /// Gets or sets the crypto stack. + /// + public BcTlsCrypto Crypto { get; set; } + + /// + /// Gets or sets the certificate of the server. + /// + public EcServerCertificate? EcCertificate { get; set; } + + /// + /// Gets or sets the CA to authorize the connecting clients. + /// + public List CAs { get; set; } = new List(); + + /// + /// Gets or sets the available cipher suites. + /// + public List CipherSuites { get; set; } = new(); + + /// + /// Gets or sets the timeout of the dtls handshake. + /// + /// + /// 0 means no timeout + /// + public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; + + /// + /// Gets or sets the provider for psk keys. + /// + public TlsPskIdentityManager? PskManager { get; set; } + + /// + /// Gets or sets the store where the session keys should be stored. + /// + /// + /// !!! ATTENTION !!! Will export session keys of communication! Only use in DEV environment. + /// + public IKeyStore? KeyStore { get; set; } +} diff --git a/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs b/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs new file mode 100644 index 0000000..834c2af --- /dev/null +++ b/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs @@ -0,0 +1,44 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + + /// + /// Represents the key file store used for decrypting TLS traffic. + /// + public class SSLKeyFileStore : IKeyStore + { + private readonly string fileName; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The filename to save the keys to. + public SSLKeyFileStore(string fileName, ILogger logger) + { + this.fileName = fileName; + this.logger = logger; + } + + /// + public void Store(DTLS12KeyFileData data) + { + try + { + using var file = File.Open(this.fileName, FileMode.Append); + using var stream = new StreamWriter(file); + stream.WriteLine($"CLIENT_RANDOM {Convert.ToHexString(data.ClientRandom)} {Convert.ToHexString(data.PreMasterSecret)}"); + } + catch (Exception ex) + { + this.logger.LogError(ex, $"Could not append session key to {this.fileName}"); + } + } + } +} diff --git a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs new file mode 100644 index 0000000..271448c --- /dev/null +++ b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs @@ -0,0 +1,31 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; +using Channel; +using Log; +using Microsoft.Extensions.Logging; + +/// +/// Represents a udp sender using a CoAP UDP channel. +/// +public class UdpChannelSender : IUDPSender +{ + private readonly UDPChannel channel; + private readonly ILogger logger = LogManager.GetLogger(); + + /// + /// Initializes a new instance of the class. + /// + /// The channel to send the data. + public UdpChannelSender(UDPChannel channel) + { + this.channel = channel; + } + + /// + public void SendTo(ReadOnlySpan payload, EndPoint remote) + { + this.logger.LogTrace("Sending {Bytes} encrypted bytes to {Remote}", payload.Length, remote); + this.channel.Send(payload.ToArray(), remote); + } +} diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs new file mode 100644 index 0000000..a2aeb05 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -0,0 +1,153 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Collections.Concurrent; +using System.Net; +using Org.BouncyCastle.Tls; + +/// +/// Represents the udp package buffer of a DTLS connection. +/// +internal class UdpTransport : DatagramTransport, IDisposable +{ + private bool disposed = false; + private readonly IUDPSender sender; + private readonly int maxPacketLength; + private readonly BlockingCollection messages = new (); + private readonly TimeSpan timeout; + private DateTimeOffset lastReceivedDatagram; + + /// + /// Initialize a new instance of the class. + /// + /// The implementation of the udp + /// The endpoint of the connection. + /// The maximum length of a dtls package. + /// The duration when the session is not valid anymore without a new datagram. + public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength, TimeSpan timeout) + { + this.Remote = remote; + this.sender = sender; + this.maxPacketLength = maxPacketLength; + this.timeout = timeout; + this.lastReceivedDatagram = DateTimeOffset.Now; + } + + /// + /// Gets the remote endpoint. + /// + public EndPoint Remote { get; } + + /// + /// Get the maximum allowed package length for receiving. + /// + /// The maximum allowed package length. + public int GetReceiveLimit() + { + return this.maxPacketLength; + } + + /// + /// Receive a udp package of the remote. + /// + /// The buffer to insert the package. + /// The offset where to insert the payload. + /// The length of the buffer. + /// The timeout of the receive operation. + /// The amount of received bytes. + public int Receive(byte[] buf, int off, int len, int waitMillis) + { + return this.Receive(buf.AsSpan(off, len), waitMillis); + } + + /// + /// Receive a udp package of the remote. + /// + /// The buffer to insert the package. + /// The timeout of the receive operation. + /// The amount of received bytes. + public int Receive(Span buffer, int waitMillis) + { + if (this.disposed) + { + throw new ObjectDisposedException(nameof(UdpTransport)); + } + + this.CheckTimeout(); + + if (this.messages.TryTake(out var rx, TimeSpan.FromMilliseconds(waitMillis))) + { + rx.CopyTo(buffer); + return rx.Length > buffer.Length ? buffer.Length : rx.Length; + } + + return 0; + } + + /// + /// Get the maximum allowed package length for sending. + /// + /// The maximum allowed package length. + public int GetSendLimit() + { + return this.maxPacketLength; + } + + /// + /// Send a package over udp. + /// + /// The buffer to send from. + /// The offset of the package in the . + /// The length of the package. + public void Send(byte[] buf, int off, int len) + { + this.Send(buf.AsSpan(off, len)); + } + + /// + /// Send a package over udp. + /// + /// The message to send. + public void Send(ReadOnlySpan buffer) + { + if (this.disposed) + { + throw new ObjectDisposedException(nameof(UdpTransport)); + } + this.CheckTimeout(); + this.sender.SendTo(buffer, this.Remote); + } + + /// + /// Close the connection. + /// + public void Close() + { + } + + /// + /// Enqueue a received message from the remote. + /// + /// The received message. + internal void Enqueue(ReadOnlySpan payload) + { + if (this.disposed) + { + throw new ObjectDisposedException(nameof(UdpTransport)); + } + this.messages.Add(payload.ToArray()); + this.lastReceivedDatagram = DateTimeOffset.Now; + } + + public void Dispose() + { + this.disposed = true; + } + + private void CheckTimeout() + { + if (DateTimeOffset.Now - this.lastReceivedDatagram > this.timeout) + { + throw new TlsTimeoutException("Did not receive a new package in time"); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj new file mode 100644 index 0000000..8ebf28e --- /dev/null +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -0,0 +1,52 @@ + + + net6.0 + enable + enable + 1.0.0-alpha.3 + World-Direct eBusiness solutions GmbH + 2023 + LICENSE + packageIcon.png + https://github.com/world-direct/CoAP.NET + + Changelog: + v0.6.3: add tracing diagnostics + v0.6.2: add session key file exporter + v0.6.1: adapt interface to interact with DTLS configuration + + + + true + snupkg + + + + + True + + + + True + + + True + + + + True + + + + + + + + + + + + + + + diff --git a/WorldDirect.CoAP.DTLS/X509CertificateExtensions.cs b/WorldDirect.CoAP.DTLS/X509CertificateExtensions.cs new file mode 100644 index 0000000..ffdffe9 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/X509CertificateExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) World-Direct eBusiness solutions GmbH. All rights reserved. + +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Security.Cryptography.X509Certificates; + using System.Text.RegularExpressions; + + public static class X509CertificateExtensions + { + public static string GetCommonName(this X509Certificate cert) + { + var regex = new Regex("CN=([\\w-]*)"); + var subject = cert.Subject; + var match = regex.Match(subject); + if (match.Success) + { + return match.Groups[1].Value; + } + + throw new ArgumentException($"Subject ({cert.Subject}) does not contain a valid common name"); + } + } +} diff --git a/WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs b/WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs new file mode 100644 index 0000000..f01639b --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs @@ -0,0 +1,105 @@ +namespace WorldDirect.CoAP.Hosting.Configuration; + +using System.Globalization; +using System.Net; + +/// +/// An address a CoAP server may bind to. +/// +public class BindingAddress +{ + + private BindingAddress(string scheme, string host, int port) + { + this.Scheme = scheme; + this.Host = host; + this.Port = port; + } + + public string Scheme { get; } + public string Host { get; set; } + public int Port { get; set; } + + public static explicit operator IPEndPoint(BindingAddress d) + { + if (d.Host == "localhost") + { + return new IPEndPoint(IPAddress.Loopback, d.Port); + } + else + { + var ipAddress = IPAddress.Any; + if (IPAddress.TryParse(d.Host, out var parsedAddress)) + { + ipAddress = parsedAddress; + } + return new IPEndPoint(ipAddress, d.Port); + } + } + + public static BindingAddress Parse(string address) + { + // A null/empty address will throw FormatException + address = address ?? string.Empty; + + var schemeDelimiterStart = address.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid url: '{address}'"); + } + var schemeDelimiterEnd = schemeDelimiterStart + Uri.SchemeDelimiter.Length; + + var pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + var pathDelimiterEnd = pathDelimiterStart; + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = address.Length; + } + + var scheme = address.Substring(0, schemeDelimiterStart); + string? host = null; + var port = 0; + + var hasSpecifiedPort = false; + + var portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) + { + var portDelimiterEnd = portDelimiterStart + ":".Length; + + var portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) + { + hasSpecifiedPort = true; + host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + port = portNumber; + } + } + + if (!hasSpecifiedPort) + { + if (string.Equals(scheme, "coap", StringComparison.OrdinalIgnoreCase)) + { + port = 5683; + } + else if (string.Equals(scheme, "coaps", StringComparison.OrdinalIgnoreCase)) + { + port = 5684; + } + } + + if (!hasSpecifiedPort) + { + host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } + + if (string.IsNullOrEmpty(host)) + { + throw new FormatException($"Invalid url: '{address}'"); + } + + return new BindingAddress(host: host, port: port, scheme: scheme); + } +} diff --git a/WorldDirect.CoAP.Hosting/Configuration/CertificateLoader.cs b/WorldDirect.CoAP.Hosting/Configuration/CertificateLoader.cs new file mode 100644 index 0000000..343bfde --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Configuration/CertificateLoader.cs @@ -0,0 +1,235 @@ +namespace WorldDirect.CoAP.Hosting.Configuration +{ + using System; + using System.Security.Cryptography.X509Certificates; + using Org.BouncyCastle.Crypto.Parameters; + using Org.BouncyCastle.OpenSsl; + using Org.BouncyCastle.Pkcs; + using Org.BouncyCastle.Security; + using Org.BouncyCastle.Tls; + using Org.BouncyCastle.Tls.Crypto; + using WorldDirect.CoAP.DTLS; + + internal class CertificateLoader + { + private readonly TlsCrypto crypto; + + public CertificateLoader(TlsCrypto crypto) + { + this.crypto = crypto; + } + + public EcServerCertificate LoadCertificate(CertificateOption config) + { + if (config.IsFromStore) + { + var ecdasCert = this.LoadCertAndKeyFromStore(config); + return ecdasCert; + } + + if (config.IsFile) + { + try + { + // *.pem and *.key file + if (!string.IsNullOrEmpty(config.Path) && !string.IsNullOrEmpty(config.KeyPath)) + { + var ecdsaCert = this.LoadCertAndKeyFromFiles(config); + return ecdsaCert; + } + // pfx file + + if (!string.IsNullOrEmpty(config.Path)) + { + var ecdsaCert = this.LoadCertAndKeyFromPfxFile(config); + return ecdsaCert; + } + throw new InvalidOperationException("Invalid configuration for certificate"); + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load certificate file {config.Path}", e); + } + } + throw new InvalidOperationException($"Invalid configuration for certificate"); + } + + public Org.BouncyCastle.X509.X509Certificate LoadCA(CertificateOption config) + { + if (config.IsFromStore) + { + var cert = this.LoadCAFromStore(config); + return cert; + } + + if (config.IsFile) + { + try + { + // pfx file, password can be empty + if (!string.IsNullOrEmpty(config.Path) && config.Password != null) + { + throw new NotImplementedException("Please provide CA in pem format."); + } + // pem file + + if (!string.IsNullOrEmpty(config.Path)) + { + var cert = this.LoadCertFromFile(config.Path!); + return cert; + } + throw new InvalidOperationException("Invalid file configuration for CA certificate."); + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load CA certificate file {config.Path}", e); + } + } + throw new InvalidOperationException($"Could not identify where to search for CA certificate."); + } + + private Org.BouncyCastle.X509.X509Certificate LoadCertFromFile(string filename) + { + using var certReader = File.OpenRead(filename); + using var certTextReader = new StreamReader(certReader); + var certPemReader = new PemReader(certTextReader); + var certObject = certPemReader.ReadObject(); + if (certObject.GetType() != typeof(Org.BouncyCastle.X509.X509Certificate)) + { + throw new InvalidOperationException($"Expected certificate in {filename}"); + } + + var cert = certObject as Org.BouncyCastle.X509.X509Certificate; + return cert!; + } + + private EcServerCertificate LoadCertAndKeyFromFiles(CertificateOption config) + { + var password = config.Password ?? string.Empty; + using var reader = File.OpenRead(config.KeyPath!); + using var textReader = new StreamReader(reader); + PemReader pemReader = new PemReader(textReader, new InMemoryPasswordFinder(password)); + + var keyObj = pemReader.ReadPemObject(); + var key = PrivateKeyFactory.CreateKey(keyObj.Content); + + if (key.GetType() != typeof(ECPrivateKeyParameters)) + { + throw new InvalidOperationException($"{config.KeyPath} does not store a EC key. Currently only ECDSA is supported"); + } + + var caPrivateKey = key as ECPrivateKeyParameters; + + var cert = this.LoadCertFromFile(config.Path!); + + var x509bc = this.crypto.CreateCertificate(cert!.GetEncoded()); + + var ecdsaCertificate = new Certificate(new[] { x509bc }); + var ecCert = new EcServerCertificate(ecdsaCertificate, caPrivateKey!); + return ecCert; + } + + private EcServerCertificate LoadCertAndKeyFromPfxFile(CertificateOption config) + { + var password = config.Password ?? string.Empty; + using var file = File.OpenRead(config.Path!); + var store = new Pkcs12StoreBuilder().Build(); + store.Load(file, password.ToCharArray()); + X509CertificateEntry? certEntry = null; + AsymmetricKeyEntry? keyEntry = null; + foreach (var alias in store.Aliases) + { + var cert = store.GetCertificate(alias); + if (cert != null) + { + certEntry = cert; + } + + var k = store.GetKey(alias); + if (k != null) + { + keyEntry = k; + } + } + + if (certEntry == null) + { + throw new InvalidOperationException($"Could not decode certificate in {config.Path}"); + } + if (keyEntry == null) + { + throw new InvalidOperationException($"Could not decode key in {config.Path}"); + } + + if (keyEntry.Key.GetType() != typeof(ECPrivateKeyParameters)) + { + throw new InvalidOperationException($"Currently on EC Key/Certificate is supported. File: {config.Path} contains invalid pair."); + } + + var x509bc = this.crypto.CreateCertificate(certEntry.Certificate.GetEncoded()); + var ecPrivateKey = (keyEntry.Key as ECPrivateKeyParameters)!; + + var ecdsaCertificate = new Certificate(new[] { x509bc }); + var ecCert = new EcServerCertificate(ecdsaCertificate, ecPrivateKey); + return ecCert; + } + + private EcServerCertificate LoadCertAndKeyFromStore(CertificateOption config) + { + var cert = CertificateManager.LoadFromStore(config.Store!, config.Location!, config.Subject!, config.AllowInvalid); + if (!cert.HasPrivateKey) + { + throw new InvalidOperationException($"Private key of {config.Subject} is missing"); + } + // other algorithms than ecdsa are currently not supported + var key = cert.GetECDsaPrivateKey(); + if (key == null) + { + throw new InvalidOperationException($"Certificate {config.Subject} is not a ECDSA Certificate."); + } + + + // need to convert to Bouncycastle Certificate + var ecdasCert = this.ToECServerCertificate(cert); + return ecdasCert; + } + + private Org.BouncyCastle.X509.X509Certificate LoadCAFromStore(CertificateOption config) + { + var cert = CertificateManager.LoadCAFromStore(config.Store!, config.Location!, config.Subject!, config.AllowInvalid); + return cert.ToBouncyCastle(); + } + + private EcServerCertificate ToECServerCertificate(X509Certificate2 certificate) + { + var certBuffer = certificate.Export(X509ContentType.Pkcs12); + var store = new Pkcs12StoreBuilder().Build(); + store.Load(new MemoryStream(certBuffer), Array.Empty()); + X509CertificateEntry? certEntry = null; + AsymmetricKeyEntry? keyEntry = null; + foreach (var alias in store.Aliases) + { + var cert = store.GetCertificate(alias); + + var k = store.GetKey(alias); + if (k != null && cert != null) + { + keyEntry = k; + certEntry = cert; + } + } + + if (certEntry == null || keyEntry == null) + { + throw new InvalidOperationException($"Could not find certificate or key for {certificate.SubjectName}"); + } + + var x509bc = this.crypto.CreateCertificate(certEntry.Certificate.GetEncoded()); + var ecPrivateKey = (keyEntry.Key as ECPrivateKeyParameters)!; + + var ecdsaCertificate = new Certificate(new[] { x509bc }); + var ecCert = new EcServerCertificate(ecdsaCertificate, ecPrivateKey); + return ecCert; + } + } +} diff --git a/WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs b/WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs new file mode 100644 index 0000000..725aeb7 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs @@ -0,0 +1,150 @@ +namespace WorldDirect.CoAP.Hosting.Configuration; + +using System.Security.Cryptography.X509Certificates; + +internal class CertificateManager +{ + private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1"; + + public static X509Certificate2 LoadFromStore(string subject, StoreName storeName, StoreLocation storeLocation, bool allowInvalid) + { + using (var store = new X509Store(storeName, storeLocation)) + { + X509Certificate2Collection? storeCertificates = null; + X509Certificate2? foundCertificate = null; + store.Open(OpenFlags.ReadOnly); + storeCertificates = store.Certificates; + foreach (var certificate in storeCertificates.Find(X509FindType.FindBySubjectName, subject, !allowInvalid) + .OfType() + .Where(IsCertificateAllowedForServerAuth) + .Where(cert => cert.HasPrivateKey) + .OrderByDescending(certificate => certificate.NotAfter)) + { + // Pick the first one if there's no exact match as a fallback to substring default. + foundCertificate ??= certificate; + + if (certificate.GetNameInfo(X509NameType.SimpleName, true).Equals(subject, StringComparison.InvariantCultureIgnoreCase)) + { + foundCertificate = certificate; + break; + } + } + + if (foundCertificate == null) + { + throw new InvalidOperationException($"Found no certificate with name {subject}"); + } + + return foundCertificate; + } + } + + + public static X509Certificate2 LoadFromStore(string name, string location, string subject, bool allowInvalid) + { + + var storeName = Enum.Parse(name); + var storeLocation = Enum.Parse(location); + + return LoadFromStore(subject, storeName, storeLocation, allowInvalid); + } + + public static X509Certificate2 LoadCAFromStore(string subject, StoreName storeName, StoreLocation storeLocation, bool allowInvalid) + { + using (var store = new X509Store(storeName, storeLocation)) + { + X509Certificate2Collection? storeCertificates = null; + X509Certificate2? foundCertificate = null; + store.Open(OpenFlags.ReadOnly); + storeCertificates = store.Certificates; + foreach (var certificate in storeCertificates.Find(X509FindType.FindBySubjectName, subject, !allowInvalid) + .OfType() + .Where(IsCertificateAllowedForCA) + .OrderByDescending(certificate => certificate.NotAfter)) + { + // Pick the first one if there's no exact match as a fallback to substring default. + foundCertificate ??= certificate; + + if (certificate.GetNameInfo(X509NameType.SimpleName, true).Equals(subject, StringComparison.InvariantCultureIgnoreCase)) + { + foundCertificate = certificate; + break; + } + } + + if (foundCertificate == null) + { + throw new InvalidOperationException($"Found no certificate with name {subject}"); + } + + return foundCertificate; + } + } + + public static X509Certificate2 LoadCAFromStore(string name, string location, string subject, bool allowInvalid) + { + + var storeName = Enum.Parse(name); + var storeLocation = Enum.Parse(location); + + return LoadCAFromStore(subject, storeName, storeLocation, allowInvalid); + } + + private static bool IsCertificateAllowedForCA(X509Certificate2 certificate) + { + + var keyUsageExtension = certificate.Extensions.OfType().FirstOrDefault(); + if (keyUsageExtension != null) + { + if ((keyUsageExtension.KeyUsages & X509KeyUsageFlags.KeyCertSign) == X509KeyUsageFlags.None) + { + return false; + } + } + + var basicConstraintExtension = certificate.Extensions.OfType().FirstOrDefault(); + if (basicConstraintExtension != null) + { + if (!basicConstraintExtension.CertificateAuthority) + { + return false; + } + } + + return true; + } + + private static bool IsCertificateAllowedForServerAuth(X509Certificate2 certificate) + { + /* If the Extended Key Usage extension is included, then we check that the serverAuth usage is included. (http://oid-info.com/get/1.3.6.1.5.5.7.3.1) + * If the Extended Key Usage extension is not included, then we assume the certificate is allowed for all usages. + * + * See also https://blogs.msdn.microsoft.com/kaushal/2012/02/17/client-certificates-vs-server-certificates/ + * + * From https://tools.ietf.org/html/rfc3280#section-4.2.1.13 "Certificate Extensions: Extended Key Usage" + * + * If the (Extended Key Usage) extension is present, then the certificate MUST only be used + * for one of the purposes indicated. If multiple purposes are + * indicated the application need not recognize all purposes indicated, + * as long as the intended purpose is present. Certificate using + * applications MAY require that a particular purpose be indicated in + * order for the certificate to be acceptable to that application. + */ + + var hasEkuExtension = false; + + foreach (var extension in certificate.Extensions.OfType()) + { + hasEkuExtension = true; + foreach (var oid in extension.EnhancedKeyUsages) + { + if (string.Equals(oid.Value, ServerAuthenticationOid, StringComparison.Ordinal)) + { + return true; + } + } + } + + return !hasEkuExtension; + } +} diff --git a/WorldDirect.CoAP.Hosting/Configuration/CertificateOption.cs b/WorldDirect.CoAP.Hosting/Configuration/CertificateOption.cs new file mode 100644 index 0000000..f73bc5a --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Configuration/CertificateOption.cs @@ -0,0 +1,17 @@ +namespace WorldDirect.CoAP.Hosting.Configuration +{ + public class CertificateOption + { + public bool IsFile => !string.IsNullOrEmpty(this.Path); + public string? Path { get; set; } + public string? KeyPath { get; set; } + public string? Password { get; set; } + + + public bool IsFromStore => !string.IsNullOrEmpty(this.Subject); + public string? Subject { get; set; } + public string? Store { get; set; } + public string Location { get; set; } = "CurrentUser"; + public bool AllowInvalid { get; set; } + } +} diff --git a/WorldDirect.CoAP.Hosting/CryptographyExtensions.cs b/WorldDirect.CoAP.Hosting/CryptographyExtensions.cs new file mode 100644 index 0000000..fb2a731 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/CryptographyExtensions.cs @@ -0,0 +1,19 @@ +namespace WorldDirect.CoAP.Hosting; + +using System.Security.Cryptography.X509Certificates; + +/// +/// Extensions methods for bouncy castle. +/// +public static class CryptographyExtensions +{ + /// + /// Convert a dotnet certificate into a bouncy castle certificate. + /// + /// The certificate to convert. + /// The corresponding bouncy castle certificate. + public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(this X509Certificate2 cert) + { + return new Org.BouncyCastle.X509.X509Certificate(cert.Export(X509ContentType.Cert)); + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CipherSuiteConfigurationCallback.cs b/WorldDirect.CoAP.Hosting/Hosting/CipherSuiteConfigurationCallback.cs new file mode 100644 index 0000000..106e856 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CipherSuiteConfigurationCallback.cs @@ -0,0 +1,7 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// The delegate to add cipher suites to a DTLS server. +/// +/// The currently enabled cipher suites. +public delegate void CipherSuiteConfigurationCallback(ISet cipherSuites); diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointBuilder.cs new file mode 100644 index 0000000..bfbfaa6 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointBuilder.cs @@ -0,0 +1,26 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// The builder to configure a coap endpoint. +/// +public class CoAPEndpointBuilder : ICoAPEndpointBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the endpoint. + /// The service collection. + public CoAPEndpointBuilder(string name, IServiceCollection services) + { + Services = services; + this.Name = name; + } + + /// + public string Name { get; } + + /// + public IServiceCollection Services { get; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointOptions.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointOptions.cs new file mode 100644 index 0000000..3d8224a --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointOptions.cs @@ -0,0 +1,12 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// The available options to configure a coap endpoint. +/// +public class CoAPEndpointOptions +{ + /// + /// Gets or sets the url the endpoint will listen on. + /// + public string Url { get; set; } = string.Empty; +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPOptions.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPOptions.cs new file mode 100644 index 0000000..db22a53 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPOptions.cs @@ -0,0 +1,17 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// The available options to configure the coap stack. +/// +public class CoAPOptions +{ + /// + /// Gets or sets the max allowed coap message size. + /// + public ushort? MaxMessageSize { get; set; } + + /// + /// Gets or sets the default block size. + /// + public ushort? DefaultBlockSize { get; set; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilder.cs new file mode 100644 index 0000000..acdbaaa --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilder.cs @@ -0,0 +1,26 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// The builder to configure a coaps endpoints. +/// +public class CoAPSEndpointBuilder : ICoAPSEndpointBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the endpoint. + /// The service collection. + public CoAPSEndpointBuilder(string name, IServiceCollection services) + { + this.Services = services; + this.Name = name; + } + + /// + public string Name { get; } + + /// + public IServiceCollection Services { get; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilderExtensions.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilderExtensions.cs new file mode 100644 index 0000000..23cb6fd --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilderExtensions.cs @@ -0,0 +1,51 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Org.BouncyCastle.Tls; +using WorldDirect.CoAP.DTLS; + +/// +/// Extensions for the . +/// +public static class CoAPSEndpointBuilderExtensions +{ + /// + /// Add pre shared key authentication for this endpoint. + /// + /// The builder of the endpoint. + /// The factory to create the . + /// The endpoint builder. + public static ICoAPSEndpointBuilder AddPreSharedKeys(this ICoAPSEndpointBuilder builder, Func factory) + { + builder.Services.TryAddTransient>(sp => new EndpointSpecific(builder.Name, factory(sp))); + return builder; + } + + /// + /// Adds the exporter of PreMasterSecrets to the endpoint. + /// + /// + /// PreMasterSecrets can be used to decipher the messages of the dtls session. + /// + /// The builder of the endpoint. + /// The factory of the to store secrets. + /// The endpoint builder. + public static ICoAPSEndpointBuilder AddPreMasterSecretExporter(this ICoAPSEndpointBuilder builder, Func factory) + { + builder.Services.TryAddSingleton>(sp => new EndpointSpecific(builder.Name, factory(sp))); + return builder; + } + + /// + /// Add enabled cipher suites to the endpoint. + /// + /// The builder of the endpoint. + /// The callback which will add cipher suites. + /// The endpoint builder. + public static ICoAPSEndpointBuilder AddCipherSuites(this ICoAPSEndpointBuilder builder, CipherSuiteConfigurationCallback callback) + { + builder.Services.AddTransient>((_) => new EndpointSpecific(builder.Name, callback)); + return builder; + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointOptions.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointOptions.cs new file mode 100644 index 0000000..f2ae3af --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointOptions.cs @@ -0,0 +1,29 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Configuration; + +/// +/// The available options to configure a coaps endpoint. +/// +public class CoAPSEndpointOptions +{ + /// + /// Gets or sets the url the endpoint will listen on. + /// + public string Url { get; set; } + + /// + /// Gets or sets the certificate used by the server. + /// + public CertificateOption? Certificate { get; set; } + + /// + /// Gets or sets the certificates used to check validity of client certificates. + /// + public List ClientCA { get; set; } = new (); + + /// + /// Gets or sets the timeout of a dtls handshake. + /// + public TimeSpan HandshakeTimeout { get; set; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilder.cs new file mode 100644 index 0000000..973a185 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilder.cs @@ -0,0 +1,21 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// The builder for the coap server. +/// +internal class CoAPServerBuilder : ICoAPServerBuilder +{ + /// + /// Initializes a new instance of the class. + /// + /// The service collection. + public CoAPServerBuilder(IServiceCollection services) + { + Services = services; + } + + /// + public IServiceCollection Services { get; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilderExtensions.cs b/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilderExtensions.cs new file mode 100644 index 0000000..4b152a5 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilderExtensions.cs @@ -0,0 +1,136 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using System.Net; +using Configuration; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using WorldDirect.CoAP.DTLS; +using WorldDirect.CoAP.Net; +using WorldDirect.CoAP.Server.Resources; + +/// +/// Extensions for the . +/// +public static class CoAPServerBuilderExtensions +{ + /// + /// Add a resource to the coap server. + /// + /// The type of the resource. + /// The server builder. + /// The server builder. + public static ICoAPServerBuilder AddResource(this ICoAPServerBuilder builder) where T : class, IResource + { + builder.Services.TryAddSingleton(); + return builder; + } + + /// + /// Adds a resource to the coap server. + /// + /// The type of the resource. + /// The server builder. + /// The factory to create the resource. + /// The server builder. + public static ICoAPServerBuilder AddResource(this ICoAPServerBuilder builder, Func factory) where T : class, IResource + { + builder.Services.TryAddSingleton(typeof(IResource), factory); + return builder; + } + + /// + /// Add an udp endpoint to the coap server. + /// + /// The server builder. + /// The name of the new endpoint. + /// The configuration. + /// A new endpoint builder. + public static ICoAPEndpointBuilder AddUdpEndpoint(this ICoAPServerBuilder builder, string name, IConfiguration configuration) + { + return builder.AddUdpEndpoint(name, configuration, null); + } + + /// + /// Add an udp endpoint to the coap server. + /// + /// The server builder. + /// The name of the new endpoint. + /// The configuration. + /// The callback to configure the . + /// The new endpoint builder. + public static ICoAPEndpointBuilder AddUdpEndpoint(this ICoAPServerBuilder builder, string name, IConfiguration configuration, + Action? configure) + { + builder.Services.Configure(name, configuration); + + builder.Services.AddSingleton((sp) => + { + var options = sp.GetRequiredService>().Get(name); + configure?.Invoke(options); + var coapConfig = sp.GetRequiredService(); + var address = (IPEndPoint)BindingAddress.Parse(options.Url); + + return new CoAPEndPoint(address, coapConfig); + }); + + return new CoAPEndpointBuilder(name, builder.Services); + } + + /// + /// Add a dtls endpoint to the coap server. + /// + /// The server builder. + /// The name of the endpoint. + /// The configuration. + /// The callback to configure the . + /// The coaps endpoint builder. + public static ICoAPSEndpointBuilder AddDTLSEndpoint(this ICoAPServerBuilder builder, string name, IConfiguration configuration, Action? configure = null) + { + builder.Services.Configure(name, configuration); + builder.Services.AddSingleton(sp => + { + var cipherSuites = new HashSet(); + var options = sp.GetRequiredService>().Get(name); + configure?.Invoke(options); + var pskManager = sp.GetServices>().SingleOrDefault(manager => manager.Name == name)?.Entity; + var cipherSuitesCallbacks = sp.GetServices>().Where(callback => callback.Name == name); + foreach (var cipherSuitesCallback in cipherSuitesCallbacks) + { + cipherSuitesCallback.Entity(cipherSuites); + } + + var crypto = new BcTlsCrypto(); + var certificateLoader = new CertificateLoader(crypto); + + var address = (IPEndPoint)BindingAddress.Parse(options.Url); + var preMasterStore = sp.GetServices>().SingleOrDefault(store => store.Name == name)?.Entity; + var coapConfig = sp.GetRequiredService(); + + var dtlsConfig = new MutableDTLSServerConfig(); + dtlsConfig.Crypto = crypto; + if (options.Certificate != null) + { + dtlsConfig.EcCertificate = certificateLoader.LoadCertificate(options.Certificate); + foreach (var certificateConfig in options.ClientCA) + { + dtlsConfig.CAs.Add(certificateLoader.LoadCA(certificateConfig)); + } + } + + dtlsConfig.CipherSuites = cipherSuites.ToList(); + dtlsConfig.KeyStore = preMasterStore; + dtlsConfig.PskManager = pskManager; + dtlsConfig.HandshakeTimeout = options.HandshakeTimeout; + var cache = sp.GetRequiredService(); + + return new CoAPSEndpoint(cache, new (dtlsConfig), address, coapConfig); + }); + + return new CoAPSEndpointBuilder(name, builder.Services); + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/EndpointSpecific.cs b/WorldDirect.CoAP.Hosting/Hosting/EndpointSpecific.cs new file mode 100644 index 0000000..2d62ca5 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/EndpointSpecific.cs @@ -0,0 +1,25 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// Default implementation of the . +/// +/// The type of the service. +public class EndpointSpecific : IEndpointSpecific +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the endpoint. + /// The service. + public EndpointSpecific(string name, T entity) + { + Name = name; + Entity = entity; + } + + /// + public string Name { get; set; } + + /// + public T Entity { get; set; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Hosting/Hosting/HostBuilderExtensions.cs b/WorldDirect.CoAP.Hosting/Hosting/HostBuilderExtensions.cs new file mode 100644 index 0000000..e216d96 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/HostBuilderExtensions.cs @@ -0,0 +1,26 @@ +namespace WorldDirect.CoAP.Hosting.Hosting +{ + using System; + using Microsoft.Extensions.Hosting; + + /// + /// Extensions for the . + /// + public static class HostBuilderExtensions + { + /// + /// Configure the coap server on the host. + /// + /// The host builder. + /// A callback to configure the coap server. + /// The host builder. + public static IHostBuilder ConfigureCoAPServer(this IHostBuilder hostBuilder, Action configure) + { + hostBuilder.ConfigureServices((ctx, services) => + { + services.AddCoAPServer((builder) => configure(ctx, builder)); + }); + return hostBuilder; + } + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/ICoAPEndpointBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/ICoAPEndpointBuilder.cs new file mode 100644 index 0000000..af9e928 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/ICoAPEndpointBuilder.cs @@ -0,0 +1,19 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// Provides an interface to build a coap endpoint. +/// +public interface ICoAPEndpointBuilder +{ + /// + /// Gets the name of the endpoint. + /// + public string Name { get; } + + /// + /// Gets the service collection. + /// + public IServiceCollection Services { get; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/ICoAPSEndpointBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/ICoAPSEndpointBuilder.cs new file mode 100644 index 0000000..23f9afc --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/ICoAPSEndpointBuilder.cs @@ -0,0 +1,8 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// Provides an interface to build a coaps endpoint. +/// +public interface ICoAPSEndpointBuilder : ICoAPEndpointBuilder +{ +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/ICoAPServerBuilder.cs b/WorldDirect.CoAP.Hosting/Hosting/ICoAPServerBuilder.cs new file mode 100644 index 0000000..a0e9a8b --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/ICoAPServerBuilder.cs @@ -0,0 +1,14 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; + +/// +/// Provides an interface to build a coap server. +/// +public interface ICoAPServerBuilder +{ + /// + /// Gets the service collection. + /// + public IServiceCollection Services { get; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/IEndpointSpecific.cs b/WorldDirect.CoAP.Hosting/Hosting/IEndpointSpecific.cs new file mode 100644 index 0000000..0ec4a2f --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/IEndpointSpecific.cs @@ -0,0 +1,18 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +/// +/// Provides an interface to add a service for a specific endpoint. +/// +/// The type of the service. +public interface IEndpointSpecific +{ + /// + /// Gets or sets the name of the endpoint this service should be used for. + /// + string Name { get; set; } + + /// + /// Gets or sets the service. + /// + T Entity { get; set; } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/ServiceCollectionExtensions.cs b/WorldDirect.CoAP.Hosting/Hosting/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..fa44432 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/ServiceCollectionExtensions.cs @@ -0,0 +1,63 @@ +namespace WorldDirect.CoAP.Hosting.Hosting; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Server; +using Services; +using WorldDirect.CoAP.Log; +using WorldDirect.CoAP.Net; +using WorldDirect.CoAP.Server.Resources; + +/// +/// Extensions for the . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Add the coap server. + /// + /// The service collection. + /// A callback to configure the coap server. + /// The service collection. + public static IServiceCollection AddCoAPServer(this IServiceCollection services, Action? configure = null) + { + services.TryAddSingleton(sp => + { + var options = ServiceProviderServiceExtensions.GetService>(sp); + var cfg = (CoapConfig)CoapConfig.Default; + if (options != null && options.Value.MaxMessageSize.HasValue) + { + cfg.MaxMessageSize = options.Value.MaxMessageSize.Value; + } + + if (options != null && options.Value.DefaultBlockSize.HasValue) + { + cfg.DefaultBlockSize = options.Value.DefaultBlockSize.Value; + } + + return cfg; + }); + services.TryAddSingleton((sp) => + { + // initialize logging for coap stack initially + LogManager.Provider = sp; + var config = sp.GetRequiredService(); + return new CoapServer(config); + }); + services.AddHostedService(sp => + { + var server = sp.GetRequiredService(); + var endpoints = sp.GetServices(); + var resources = sp.GetServices(); + var lifetime = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + return new CoAPServerService(server, endpoints, resources, sp, lifetime, logger); + }); + + configure?.Invoke(new CoAPServerBuilder(services)); + return services; + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/Services/CoAPServerService.cs b/WorldDirect.CoAP.Hosting/Hosting/Services/CoAPServerService.cs new file mode 100644 index 0000000..3547be7 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/Services/CoAPServerService.cs @@ -0,0 +1,51 @@ +namespace WorldDirect.CoAP.Hosting.Hosting.Services; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using WorldDirect.CoAP.Log; +using WorldDirect.CoAP.Net; +using WorldDirect.CoAP.Server; +using WorldDirect.CoAP.Server.Resources; + +/// +/// The service whose purpose is to run the coap server. +/// +internal class CoAPServerService : VitalBackgroundService +{ + private readonly ILogger logger; + private readonly CoapServer server; + + /// + /// Initializes a new instance of the class. + /// + /// The server that will be run. + /// The endpoints the server is available on. + /// The resources of the server. + /// The service provider to create services. + /// The lifetime. + /// The logger. + public CoAPServerService(CoapServer server, IEnumerable endpoints, IEnumerable resources, IServiceProvider serviceProvider, IHostApplicationLifetime lifetime, ILogger logger) : base(lifetime, logger) + { + // initialize logger for coap stack + LogManager.Provider = serviceProvider; + this.logger = logger; + this.server = server; + foreach (var endpoint in endpoints) + { + this.server.AddEndPoint(endpoint); + } + + this.server.Add(resources.ToArray()); + } + + /// + protected override Task ExecuteAsync(CancellationToken ct) + { + ct.Register(() => server.Stop()); + server.Start(); + + logger.LogInformation("CoAP Server started on {@LocalEndpoints}", server.EndPoints.Select(e => e.LocalEndPoint.ToString())); + + return Task.CompletedTask; + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/Services/VitalBackgroundService.cs b/WorldDirect.CoAP.Hosting/Hosting/Services/VitalBackgroundService.cs new file mode 100644 index 0000000..9f30436 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/Services/VitalBackgroundService.cs @@ -0,0 +1,141 @@ +// Copyright (c) World-Direct eBusiness solutions GmbH. All rights reserved. + +namespace WorldDirect.CoAP.Hosting.Hosting.Services +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + + /// + /// Represents a that is vital for the application. + /// If the services crashes, an application shutdown is initiated. + /// + /// + public abstract class VitalBackgroundService : IHostedService + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly IHostApplicationLifetime lifetime; + private readonly ILogger logger; + private bool disposed = false; + private Task? service; + + /// + /// Initializes a new instance of the class. + /// + /// The lifetime manage of the application. + /// The logger to log events of interest. + protected VitalBackgroundService(IHostApplicationLifetime lifetime, ILogger logger) + { + this.lifetime = lifetime; + this.logger = logger; + } + + /// + /// Triggered when the application host is ready to start the service. + /// + /// Indicates that the start process has been aborted. + /// A task that completes after the service has started. + public Task StartAsync(CancellationToken cancellationToken) + { + logger.LogInformation("{ServiceName} starting.", GetType().Name); + + // Store the task we're executing + service = RunServiceAsync(cts.Token); + + // If the task is completed then return it, this will bubble cancellation and failure to the caller + if (service.IsCompleted) + { + return service; + } + + logger.LogInformation("{ServiceName} started.", GetType().Name); + + // Otherwise it's running + return Task.CompletedTask; + } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// + /// Indicates that the shutdown process should no longer be graceful. + /// A that completes after the service has been stopped. + public async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogDebug("{ServiceName} stopping.", GetType().Name); + + // Stop called without start + if (service != null) + { + try + { + // Signal cancellation to the executing method + cts.Cancel(); + } + finally + { + // Wait until the task completes or the stop token triggers + await Task.WhenAny(service, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false); + } + } + + logger.LogDebug("{ServiceName} stopped.", GetType().Name); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + if (disposed) + { + return; + } + + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// This method is called when the starts. The implementation should return a task that represents + /// the lifetime of the long running operation(s) being performed. + /// + /// Triggered when is called. + /// A that represents the long running operations. + protected abstract Task ExecuteAsync(CancellationToken ct); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + cts?.Dispose(); + } + + disposed = true; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "No exception should escape.")] + private async Task RunServiceAsync(CancellationToken cancellationToken) + { + try + { + await Task.Yield(); + await ExecuteAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) + { + logger.LogError(e, "{ServiceName} crashed. Shutdown application.", GetType().Name); + lifetime.StopApplication(); + } + } + } +} diff --git a/WorldDirect.CoAP.Hosting/Hosting/TraceProviderBuilderExtensions.cs b/WorldDirect.CoAP.Hosting/Hosting/TraceProviderBuilderExtensions.cs new file mode 100644 index 0000000..54ae4bd --- /dev/null +++ b/WorldDirect.CoAP.Hosting/Hosting/TraceProviderBuilderExtensions.cs @@ -0,0 +1,21 @@ +namespace WorldDirect.CoAP.Hosting.Hosting +{ + using OpenTelemetry.Trace; + + /// + /// Extensions for the . + /// + public static class TraceProviderBuilderExtensions + { + /// + /// Add the coap instrumentation to tracing. + /// + /// The trace builder. + /// The trace builder. + public static TracerProviderBuilder AddCoAPInstrumentation(this TracerProviderBuilder builder) + { + builder.AddSource(Tracing.ActivityName); + return builder; + } + } +} diff --git a/WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs b/WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs new file mode 100644 index 0000000..802e8b9 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs @@ -0,0 +1,25 @@ +namespace WorldDirect.CoAP.Hosting; + +using Org.BouncyCastle.OpenSsl; + +/// +/// A helper class for decrypting of files. +/// +internal class InMemoryPasswordFinder : IPasswordFinder +{ + private readonly string password; + /// + /// Initializes a new instance of the class. + /// + /// The password. + public InMemoryPasswordFinder(string password) + { + this.password = password; + } + + /// + public char[] GetPassword() + { + return this.password.ToCharArray(); + } +} diff --git a/WorldDirect.CoAP.Hosting/README.md b/WorldDirect.CoAP.Hosting/README.md new file mode 100644 index 0000000..c0c6db6 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/README.md @@ -0,0 +1,88 @@ +# Future improvements + +- Enable RSA Certificates +- Load own Certificate Chain from store +- PSK client authorization + +# Examples + +## CoAP Server Config Unsecure +``` json + "Coap": { + "Endpoints": { + "CoAP": { + "Url": "coap://*:5683" + } + } + } +``` + +## CoAPS Server Config with Certificate from pfx file without client authentication +``` json + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Path": "server.pfx", + "Password": "$CREDENTIAL_PLACEHOLDER$" + }, + "HandshakeTimeout": "00:01:00" + } + } + } +``` + +## CoAPS Server Config with Certificate from .pem and encrypted .key file without client authentication +``` json + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Path": "server-cert.pem", + "KeyPath": "server-key.key" + "Password": "$CREDENTIAL_PLACEHOLDER$" + }, + "HandshakeTimeout": "00:01:00" + } + } + } +``` + +## CoAPS Server Config with Certificate from store without client authentication +``` json + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Subject": "ls1.argus.dev.energy.loc", + "Store": "", + "Location": "", + "AllowInvalid": "" + }, + "HandshakeTimeout": "00:01:00" + } + } + } +``` + +## CoAPS Server Config with Certificate from pfx file and CA from file +``` json + "Coap": { + "Endpoints": { + "CoAP": { + "Url": "coaps://*:5684", + "Certificate": { + "Path": "server.p12", + "Password": "lukas!" + }, + "ClientCA": { + "Path": "ca-cert.pem" + }, + "HandshakeTimeout": "00:01:00" + } + } + } +``` \ No newline at end of file diff --git a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj new file mode 100644 index 0000000..0db6406 --- /dev/null +++ b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj @@ -0,0 +1,60 @@ + + + + net6.0 + enable + enable + 1.0.0-alpha.3 + World-Direct eBusiness solutions GmbH + 2023 + LICENSE + packageIcon.png + https://github.com/world-direct/CoAP.NET + + Changelog: + v0.6.3: add tracing diagnostics + v0.6.2: add ssl keyfile for extraction of session keys + v0.6.1: adapt to easier configuration of dtls + + + + + true + snupkg + + + + + True + + + + True + + + True + + + + True + + + + + + + + + + + + + + + + + + + + + diff --git a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs index dd717ee..5a0592d 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs @@ -15,6 +15,7 @@ namespace WorldDirect.CoAP.Channel using System.Net; using System.Net.Sockets; using System.Transactions; + using Microsoft.Extensions.Logging; public partial class UDPChannel { @@ -65,7 +66,7 @@ private void BeginSend(UDPSocket socket, Byte[] data, System.Net.EndPoint destin if (destination is IPEndPoint ep) { - log.Debug(message: $"Sending packet to {ep.Address.MapToIPv4()}:{ep.Port}. Processing package {(completedSynchronous ? "asynchronous" : "synchronous")}"); + log.LogTrace(message: $"Sending packet to {ep.Address.MapToIPv4()}:{ep.Port}. Processing package {(completedSynchronous ? "asynchronous" : "synchronous")}"); } if (completedSynchronous) @@ -77,15 +78,23 @@ private void BeginSend(UDPSocket socket, Byte[] data, System.Net.EndPoint destin private void ProcessReceive(SocketAsyncEventArgs e, bool requeue) { UDPSocket socket = (UDPSocket)e.UserToken; - if (e.SocketError == SocketError.Success) { + if(e.BytesTransferred > this.ReceivePacketSizeToReport) + { + var base64Payload = Convert.ToBase64String(e.Buffer.AsSpan().Slice(e.Offset, e.BytesTransferred)); + log.LogWarning("Received a message greater than expected from {Remote}: {Payload}", e.RemoteEndPoint, base64Payload); + } EndReceive(socket, e.Buffer, e.Offset, e.BytesTransferred, e.RemoteEndPoint); } else if (e.SocketError != SocketError.OperationAborted && e.SocketError != SocketError.Interrupted) { - throw new SocketException((Int32)e.SocketError); + if(e.SocketError != SocketError.MessageSize) + { + throw new SocketException((Int32)e.SocketError); + } + log.LogError(new SocketException((Int32)e.SocketError), "A udp message greater {BufferSize} was received", e.Buffer.Length); } if (requeue) diff --git a/WorldDirect.CoAP/Channel/UDPChannel.cs b/WorldDirect.CoAP/Channel/UDPChannel.cs index 8de9cbe..bb98db4 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.cs @@ -17,6 +17,7 @@ namespace WorldDirect.CoAP.Channel using System.Net.Sockets; using System.Threading; using Log; + using Microsoft.Extensions.Logging; /// /// Channel via UDP protocol. @@ -24,7 +25,7 @@ namespace WorldDirect.CoAP.Channel public partial class UDPChannel : IChannel { - static readonly ILogger log = LogManager.GetLogger(typeof(UDPChannel)); + private readonly ILogger log = LogManager.GetLogger(); /// /// Default size of buffer for receiving packet. @@ -110,6 +111,12 @@ public Int32 ReceivePacketSize set { _receivePacketSize = value; } } + /// + /// Gets or sets the packet size that should be reported and logged to investigate how large messages are created. + /// The default value is 1500. + /// + public Int32 ReceivePacketSizeToReport { get; set; } = 1500; + /// public void Start() { @@ -239,6 +246,7 @@ private void EndReceive(UDPSocket socket, Byte[] buffer, Int32 offset, Int32 cou { if (count > 0) { + Metrics.Log.BytesReceived(count); Byte[] bytes = new Byte[count]; Buffer.BlockCopy(buffer, 0, bytes, 0, count); @@ -252,13 +260,13 @@ private void EndReceive(UDPSocket socket, Byte[] buffer, Int32 offset, Int32 cou try { DateTimeOffset start = DateTimeOffset.Now; - log.Info($"UDP-FireDataReceived START"); + log.LogTrace($"UDP-FireDataReceived START"); FireDataReceived(bytes, ep); - log.Info($"UDP-FireDataReceived END ({DateTimeOffset.Now - start})"); + log.LogTrace("UDP-FireDataReceived END ({Duration})", DateTimeOffset.Now - start); } catch (Exception e) { - log.Error($"FireDataReceived error occurred: {e.ToString()}", e); + log.LogError($"FireDataReceived error occurred: {e.ToString()}", e); } } } @@ -309,6 +317,7 @@ private void BeginSend() } } + Metrics.Log.BytesTransmitted(raw.Data.Length); BeginSend(socket, raw.Data, remoteEndPoint); } while (messageDequeued); diff --git a/WorldDirect.CoAP/CoapClient.cs b/WorldDirect.CoAP/CoapClient.cs index 32f7913..e2c36b1 100644 --- a/WorldDirect.CoAP/CoapClient.cs +++ b/WorldDirect.CoAP/CoapClient.cs @@ -2,9 +2,11 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Log; + using Microsoft.Extensions.Logging; using Net; /// @@ -15,7 +17,7 @@ public class CoapClient #region Locals private static readonly IEnumerable EmptyLinks = new WebLink[0]; - private static ILogger log = LogManager.GetLogger(typeof(CoapClient)); + private static ILogger log = LogManager.GetLogger(); private ICoapConfig _config; private IEndPoint _endpoint; @@ -458,23 +460,54 @@ public Response Send(Request request) return Prepare(request).Send().WaitForResponse(_timeout); } - public void SendAsync(Request request, Action done, Action fail = null) + private void SendAsync(Request request, Action done, Action fail = null) { request.Respond += (o, e) => Deliver(done, e); request.Rejected += (o, e) => Fail(fail, FailReason.Rejected); request.TimedOut += (o, e) => Fail(fail, FailReason.TimedOut); - Prepare(request).Send(); + request.Send(); } public Task SendAsync(Request request, CancellationToken ct) { + request = Prepare(request); + var activity = Tracing.ClientSource.StartActivity("CoAP Request"); + activity?.AddTag("coap.method", request.Method); + activity?.AddTag("coap.uri", request.URI); + activity?.AddTag("coap.resource", request.UriPath); + activity?.AddTag("coap.remote", request.Destination); + if (activity != null) + { + request.Retransmitting += (o, ev) => + { + activity.AddEvent(new ActivityEvent("Retransmitting")); + }; + request.Responding += (obj, ev) => + { + var response = ev.Response; + if (response.Block1 != null) + { + activity.AddEvent(new ActivityEvent($"New Block {ev.Response.Block1?.NUM}")); + } + else if (response.Block2 != null) + { + activity.AddEvent(new ActivityEvent($"New Block {ev.Response.Block2?.NUM}")); + } + }; + } TaskCompletionSource tcs = new TaskCompletionSource(); - var cancellation = ct.Register(() => tcs.TrySetCanceled(ct)); + var cancellation = ct.Register(() => + { + tcs.TrySetCanceled(ct); + request.Cancel(); + }); Action success = (r) => { + activity?.AddTag("coap.statuscode", r.StatusCode); + activity?.Stop(); tcs.TrySetResult(r); cancellation.Dispose(); }; @@ -485,11 +518,13 @@ public Task SendAsync(Request request, CancellationToken ct) if (fr == FailReason.TimedOut) { + activity?.AddTag("coap.statuscode", "TIMEOUT"); exception = new TimeoutException(); } else if (fr == FailReason.Rejected) { + activity?.AddTag("coap.statuscode", "REJECTED"); exception = new InvalidOperationException("The request has been rejected."); } @@ -498,6 +533,7 @@ public Task SendAsync(Request request, CancellationToken ct) exception = new InvalidOperationException($"The request failed with the reason {fr}"); } + activity?.Stop(); tcs.TrySetException(exception); cancellation.Dispose(); }; @@ -587,8 +623,7 @@ private Request CreateObservationRequest(Request request, Action notif } else { - if (log.IsDebugEnabled) - log.Debug("Dropping old notification: " + resp); + log.LogDebug("Dropping old notification: " + resp); } } }; diff --git a/WorldDirect.CoAP/CoapConfig.cs b/WorldDirect.CoAP/CoapConfig.cs index 01b8f74..28498ed 100644 --- a/WorldDirect.CoAP/CoapConfig.cs +++ b/WorldDirect.CoAP/CoapConfig.cs @@ -62,7 +62,7 @@ public static ICoapConfig Default private Int32 _notificationReregistrationBackoff = 2000; // ms private Int32 _channelReceiveBufferSize; private Int32 _channelSendBufferSize; - private Int32 _channelReceivePacketSize = 2048; + private Int32 _channelReceivePacketSize = 4096; /// /// Instantiate. diff --git a/WorldDirect.CoAP/Codec/DatagramWriter.cs b/WorldDirect.CoAP/Codec/DatagramWriter.cs index 971469d..5449ce8 100644 --- a/WorldDirect.CoAP/Codec/DatagramWriter.cs +++ b/WorldDirect.CoAP/Codec/DatagramWriter.cs @@ -14,13 +14,14 @@ namespace WorldDirect.CoAP.Codec using System; using System.IO; using Log; + using Microsoft.Extensions.Logging; /// /// This class describes the functionality to write raw network-ordered datagrams on bit-level. /// public class DatagramWriter { - private static ILogger log = LogManager.GetLogger(typeof(DatagramWriter)); + private static ILogger log = LogManager.GetLogger(); private MemoryStream _stream; private Byte _currentByte; @@ -45,8 +46,7 @@ public void Write(Int32 data, Int32 numBits) { if (numBits < 32 && data >= (1 << numBits)) { - if (log.IsWarnEnabled) - log.Warn(String.Format("Truncating value {0} to {1}-bit integer", data, numBits)); + log.LogWarning(String.Format("Truncating value {0} to {1}-bit integer", data, numBits)); } for (Int32 i = numBits - 1; i >= 0; i--) diff --git a/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs b/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs index daaa932..046d3f6 100644 --- a/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs +++ b/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs @@ -13,10 +13,11 @@ namespace WorldDirect.CoAP.Deduplication { using System; using Log; + using Microsoft.Extensions.Logging; static class DeduplicatorFactory { - static readonly ILogger log = LogManager.GetLogger(typeof(DeduplicatorFactory)); + static readonly ILogger log = LogManager.GetLogger(); public const String MarkAndSweepDeduplicator = "MarkAndSweep"; public const String CropRotationDeduplicator = "CropRotation"; public const String NoopDeduplicator = "Noop"; @@ -33,8 +34,7 @@ public static IDeduplicator CreateDeduplicator(ICoapConfig config) else if (!String.Equals(NoopDeduplicator, type, StringComparison.OrdinalIgnoreCase) && !String.Equals("NO_DEDUPLICATOR", type, StringComparison.OrdinalIgnoreCase)) { - if (log.IsWarnEnabled) - log.Warn("Unknown deduplicator type: " + type); + log?.LogWarning("Unknown deduplicator type: " + type); } return new NoopDeduplicator(); } diff --git a/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs b/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs index 6ad550c..a3c9282 100644 --- a/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs +++ b/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs @@ -14,16 +14,19 @@ namespace WorldDirect.CoAP.Deduplication using System; using System.Collections.Concurrent; using System.Collections.Generic; + using System.Net; using System.Threading; using Log; + using Microsoft.Extensions.Logging; using Net; class SweepDeduplicator : IDeduplicator { - static readonly ILogger log = LogManager.GetLogger(typeof(SweepDeduplicator)); + static readonly ILogger log = LogManager.GetLogger(); - private ConcurrentDictionary _incommingMessages - = new ConcurrentDictionary(); + private ConcurrentDictionary _incomingMessages = new ConcurrentDictionary(); + private ConcurrentDictionary _outgoingMessages = new ConcurrentDictionary(); + private Timer _timer; private ICoapConfig _config; @@ -34,17 +37,21 @@ public SweepDeduplicator(ICoapConfig config) private void Sweep(object state) { - if (log.IsDebugEnabled) - log.Debug("Start Mark-And-Sweep with " + _incommingMessages.Count + " entries"); + log.LogTrace("Start Mark-And-Sweep with " + (_incomingMessages.Count + _outgoingMessages.Count) + " entries"); + this.Sweep(this._incomingMessages); + this.Sweep(this._outgoingMessages); + } + + private void Sweep(ConcurrentDictionary dict) + { DateTime oldestAllowed = DateTime.Now.AddMilliseconds(-_config.ExchangeLifetime); List keysToRemove = new List(); - foreach (KeyValuePair pair in _incommingMessages) + foreach (KeyValuePair pair in dict) { if (pair.Value.Timestamp < oldestAllowed) { - if (log.IsDebugEnabled) - log.Debug("Mark-And-Sweep removes " + pair.Key); + log.LogTrace("Mark-And-Sweep removes " + pair.Key); keysToRemove.Add(pair.Key); } } @@ -53,7 +60,7 @@ private void Sweep(object state) Exchange ex; foreach (Exchange.KeyID key in keysToRemove) { - _incommingMessages.TryRemove(key, out ex); + dict.TryRemove(key, out ex); } } } @@ -74,18 +81,30 @@ public void Stop() /// public void Clear() { - _incommingMessages.Clear(); + _incomingMessages.Clear(); } /// public Exchange FindPrevious(Exchange.KeyID key, Exchange exchange) { + Exchange prev = null; - _incommingMessages.AddOrUpdate(key, exchange, (k, v) => + if (exchange.Origin == Origin.Local) { - prev = v; - return exchange; - }); + _outgoingMessages.AddOrUpdate(key, exchange, (k, v) => + { + prev = v; + return exchange; + }); + } + else + { + _incomingMessages.AddOrUpdate(key, exchange, (k, v) => + { + prev = v; + return exchange; + }); + } return prev; } @@ -93,7 +112,7 @@ public Exchange FindPrevious(Exchange.KeyID key, Exchange exchange) public Exchange Find(Exchange.KeyID key) { Exchange prev; - _incommingMessages.TryGetValue(key, out prev); + _outgoingMessages.TryGetValue(key, out prev); return prev; } diff --git a/WorldDirect.CoAP/EndPoint/Resources/Resource.cs b/WorldDirect.CoAP/EndPoint/Resources/Resource.cs index e444097..66eb135 100644 --- a/WorldDirect.CoAP/EndPoint/Resources/Resource.cs +++ b/WorldDirect.CoAP/EndPoint/Resources/Resource.cs @@ -15,13 +15,14 @@ namespace WorldDirect.CoAP.EndPoint.Resources using System.Collections.Generic; using System.Text; using Log; + using Microsoft.Extensions.Logging; /// /// This class describes the functionality of a CoAP resource. /// public abstract class Resource : IComparable { - private static ILogger log = LogManager.GetLogger(typeof(Resource)); + private static ILogger log = LogManager.GetLogger(); private Int32 _totalSubResourceCount; private String _resourceIdentifier; @@ -345,8 +346,7 @@ public void AddSubResource(Resource resource) { if (_parent != null) { - if (log.IsWarnEnabled) - log.Warn("Adding absolute path only allowed for root: made relative " + resource.Name); + log.LogWarning("Adding absolute path only allowed for root: made relative " + resource.Name); } resource.Name = resource.Name.Substring(1); } @@ -366,8 +366,7 @@ public void AddSubResource(Resource resource) if (path.Length == 0) { // resource replaces base - if (log.IsInfoEnabled) - log.Info("Replacing resource " + baseRes.Path); + log.LogInformation("Replacing resource " + baseRes.Path); foreach (Resource sub in baseRes.GetSubResources()) { sub._parent = resource; @@ -383,8 +382,7 @@ public void AddSubResource(Resource resource) String[] segments = path.Split('/'); if (segments.Length > 1) { - if (log.IsDebugEnabled) - log.Debug("Splitting up compound resource " + resource.Name); + log.LogDebug("Splitting up compound resource " + resource.Name); resource.Name = segments[segments.Length - 1]; // insert middle segments @@ -403,8 +401,7 @@ public void AddSubResource(Resource resource) resource._parent = baseRes; baseRes.SubResources[resource.Name] = resource; - if (log.IsDebugEnabled) - log.Debug("Add resource " + resource.Name); + log.LogDebug("Add resource " + resource.Name); } // update number of sub-resources in the tree diff --git a/WorldDirect.CoAP/LinkAttribute.cs b/WorldDirect.CoAP/LinkAttribute.cs index 926db60..3480e4a 100644 --- a/WorldDirect.CoAP/LinkAttribute.cs +++ b/WorldDirect.CoAP/LinkAttribute.cs @@ -14,13 +14,14 @@ namespace WorldDirect.CoAP using System; using System.Text; using Log; + using Microsoft.Extensions.Logging; /// /// Class for linkformat attributes. /// public class LinkAttribute : IComparable { - private static readonly ILogger log = LogManager.GetLogger(typeof(LinkAttribute)); + private static readonly ILogger log = LogManager.GetLogger(); private String _name; private Object _value; @@ -98,8 +99,7 @@ public void Serialize(StringBuilder builder) } else { - if (log.IsErrorEnabled) - log.Error(String.Format("Serializing attribute of unexpected type: {0} ({1})", _name, _value.GetType().Name)); + log.LogError(String.Format("Serializing attribute of unexpected type: {0} ({1})", _name, _value.GetType().Name)); } } } diff --git a/WorldDirect.CoAP/LinkFormat.cs b/WorldDirect.CoAP/LinkFormat.cs index 60a5ac6..baedc4b 100644 --- a/WorldDirect.CoAP/LinkFormat.cs +++ b/WorldDirect.CoAP/LinkFormat.cs @@ -17,6 +17,7 @@ namespace WorldDirect.CoAP using System.Text.RegularExpressions; using EndPoint.Resources; using Log; + using Microsoft.Extensions.Logging; using Server.Resources; using Util; using Resource = EndPoint.Resources.Resource; @@ -75,7 +76,7 @@ public static class LinkFormat static readonly Regex EqualRegex = new Regex("="); static readonly Regex BlankRegex = new Regex("\\s"); - private static ILogger log = LogManager.GetLogger(typeof(LinkFormat)); + private static ILogger log = LogManager.GetLogger(); public static String Serialize(IResource root) { @@ -469,8 +470,7 @@ internal static Boolean AddAttribute(ICollection attributes, Link { if (attr.Name.Equals(attrToAdd.Name)) { - if (log.IsDebugEnabled) - log.Debug("Found existing singleton attribute: " + attr.Name); + log?.LogDebug("Found existing singleton attribute: " + attr.Name); return false; } } diff --git a/WorldDirect.CoAP/Log/CommonLoggingManager.cs b/WorldDirect.CoAP/Log/CommonLoggingManager.cs deleted file mode 100644 index 9a94abd..0000000 --- a/WorldDirect.CoAP/Log/CommonLoggingManager.cs +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2011-2014, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - -namespace WorldDirect.CoAP.Log -{ - using System; - - class CommonLoggingManager : ILogManager - { - public ILogger GetLogger(Type type) - { - return new CommonLogging(Common.Logging.LogManager.GetLogger(type)); - } - - public ILogger GetLogger(String name) - { - return new CommonLogging(Common.Logging.LogManager.GetLogger(name)); - } - - class CommonLogging : ILogger - { - private readonly Common.Logging.ILog _log; - - public CommonLogging(Common.Logging.ILog log) - { - _log = log; - } - - public void Debug(Object message, Exception exception) - { - _log.Debug(message, exception); - } - - public void Debug(Object message) - { - _log.Debug(message); - } - - public void DebugFormat(IFormatProvider provider, String format, params Object[] args) - { - _log.DebugFormat(provider, format, args); - } - - public void DebugFormat(String format, Object arg0, Object arg1, Object arg2) - { - _log.DebugFormat(format, arg0, arg1, arg2); - } - - public void DebugFormat(String format, Object arg0, Object arg1) - { - _log.DebugFormat(format, arg0, arg1); - } - - public void DebugFormat(String format, Object arg0) - { - _log.DebugFormat(format, arg0); - } - - public void DebugFormat(String format, params Object[] args) - { - _log.DebugFormat(format, args); - } - - public void Error(Object message, Exception exception) - { - _log.Error(message, exception); - } - - public void Error(Object message) - { - _log.Error(message); - } - - public void ErrorFormat(IFormatProvider provider, String format, params Object[] args) - { - _log.ErrorFormat(provider, format, args); - } - - public void ErrorFormat(String format, Object arg0, Object arg1, Object arg2) - { - _log.ErrorFormat(format, arg0, arg1, arg2); - } - - public void ErrorFormat(String format, Object arg0, Object arg1) - { - _log.ErrorFormat(format, arg0, arg1); - } - - public void ErrorFormat(String format, Object arg0) - { - _log.ErrorFormat(format, arg0); - } - - public void ErrorFormat(String format, params Object[] args) - { - _log.ErrorFormat(format, args); - } - - public void Fatal(Object message, Exception exception) - { - _log.Fatal(message, exception); - } - - public void Fatal(Object message) - { - _log.Fatal(message); - } - - public void FatalFormat(IFormatProvider provider, String format, params Object[] args) - { - _log.FatalFormat(provider, format, args); - } - - public void FatalFormat(String format, Object arg0, Object arg1, Object arg2) - { - _log.FatalFormat(format, arg0, arg1, arg2); - } - - public void FatalFormat(String format, Object arg0, Object arg1) - { - _log.FatalFormat(format, arg0, arg1); - } - - public void FatalFormat(String format, Object arg0) - { - _log.FatalFormat(format, arg0); - } - - public void FatalFormat(String format, params Object[] args) - { - _log.FatalFormat(format, args); - } - - public void Info(Object message, Exception exception) - { - _log.Info(message, exception); - } - - public void Info(Object message) - { - _log.Info(message); - } - - public void InfoFormat(IFormatProvider provider, String format, params Object[] args) - { - _log.InfoFormat(provider, format, args); - } - - public void InfoFormat(String format, Object arg0, Object arg1, Object arg2) - { - _log.InfoFormat(format, arg0, arg1, arg2); - } - - public void InfoFormat(String format, Object arg0, Object arg1) - { - _log.InfoFormat(format, arg0, arg1); - } - - public void InfoFormat(String format, Object arg0) - { - _log.InfoFormat(format, arg0); - } - - public void InfoFormat(String format, params Object[] args) - { - _log.InfoFormat(format, args); - } - - public Boolean IsDebugEnabled - { - get { return LogLevel.Debug >= LogManager.Level && _log.IsDebugEnabled; } - } - - public Boolean IsErrorEnabled - { - get { return LogLevel.Error >= LogManager.Level && _log.IsErrorEnabled; } - } - - public Boolean IsFatalEnabled - { - get { return LogLevel.Fatal >= LogManager.Level && _log.IsFatalEnabled; } - } - - public Boolean IsInfoEnabled - { - get { return LogLevel.Info >= LogManager.Level && _log.IsInfoEnabled; } - } - - public Boolean IsWarnEnabled - { - get { return LogLevel.Warning >= LogManager.Level && _log.IsWarnEnabled; } - } - - public void Warn(Object message, Exception exception) - { - _log.Warn(message, exception); - } - - public void Warn(Object message) - { - _log.Warn(message); - } - - public void WarnFormat(IFormatProvider provider, String format, params Object[] args) - { - _log.WarnFormat(provider, format, args); - } - - public void WarnFormat(String format, Object arg0, Object arg1, Object arg2) - { - _log.WarnFormat(format, arg0, arg1, arg2); - } - - public void WarnFormat(String format, Object arg0, Object arg1) - { - _log.WarnFormat(format, arg0, arg1); - } - - public void WarnFormat(String format, Object arg0) - { - _log.WarnFormat(format, arg0); - } - - public void WarnFormat(String format, params Object[] args) - { - _log.WarnFormat(format, args); - } - } - } -} diff --git a/WorldDirect.CoAP/Log/ConsoleLogManager.cs b/WorldDirect.CoAP/Log/ConsoleLogManager.cs deleted file mode 100644 index 7f326bb..0000000 --- a/WorldDirect.CoAP/Log/ConsoleLogManager.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2011-2014, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - -namespace WorldDirect.CoAP.Log -{ - using System; - - class ConsoleLogManager : ILogManager - { - static readonly ILogger _logger = new TextWriterLogger(Console.Out); - - public ILogger GetLogger(Type type) - { - return _logger; - } - - public ILogger GetLogger(string name) - { - return _logger; - } - } -} diff --git a/WorldDirect.CoAP/Log/ILogManager.cs b/WorldDirect.CoAP/Log/ILogManager.cs deleted file mode 100644 index e301ce6..0000000 --- a/WorldDirect.CoAP/Log/ILogManager.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2011-2014, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - -namespace WorldDirect.CoAP.Log -{ - using System; - - /// - /// Provides methods to acquire . - /// - public interface ILogManager - { - /// - /// Gets a logger of the given type. - /// - ILogger GetLogger(Type type); - /// - /// Gets a named logger. - /// - ILogger GetLogger(String name); - } -} diff --git a/WorldDirect.CoAP/Log/ILogger.cs b/WorldDirect.CoAP/Log/ILogger.cs deleted file mode 100644 index cb2dd1c..0000000 --- a/WorldDirect.CoAP/Log/ILogger.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2011-2012, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - -namespace WorldDirect.CoAP.Log -{ - using System; - - /// - /// Provides methods to log messages. - /// - public interface ILogger - { - /// - /// Is debug enabled? - /// - Boolean IsDebugEnabled { get; } - /// - /// Is error enabled? - /// - Boolean IsErrorEnabled { get; } - /// - /// Is fatal enabled? - /// - Boolean IsFatalEnabled { get; } - /// - /// Is info enabled? - /// - Boolean IsInfoEnabled { get; } - /// - /// Is warning enabled? - /// - Boolean IsWarnEnabled { get; } - /// - /// Logs a debug message. - /// - void Debug(Object message); - /// - /// Logs a debug message. - /// - void Debug(Object message, Exception exception); - /// - /// Logs an error message. - /// - void Error(Object message); - /// - /// Logs an error message. - /// - void Error(Object message, Exception exception); - /// - /// Logs a fatal message. - /// - void Fatal(Object message); - /// - /// Logs a fatal message. - /// - void Fatal(Object message, Exception exception); - /// - /// Logs an info message. - /// - void Info(Object message); - /// - /// Logs an info message. - /// - void Info(Object message, Exception exception); - /// - /// Logs a warning message. - /// - void Warn(Object message); - /// - /// Logs a warning message. - /// - void Warn(Object message, Exception exception); - } -} diff --git a/WorldDirect.CoAP/Log/LogManager.cs b/WorldDirect.CoAP/Log/LogManager.cs index 8992f88..5d66942 100644 --- a/WorldDirect.CoAP/Log/LogManager.cs +++ b/WorldDirect.CoAP/Log/LogManager.cs @@ -12,97 +12,33 @@ namespace WorldDirect.CoAP.Log { using System; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.Logging; /// /// Log manager. /// public static class LogManager { - static LogLevel _level = LogLevel.All; - static ILogManager _manager; static LogManager() { - Type test; - try - { - test = Type.GetType("Common.Logging.LogManager, Common.Logging"); - } - catch - { - test = null; - } - - _manager = NopLogManager.Instance; - } - - /// - /// Gets or sets the global log level. - /// - public static LogLevel Level - { - get { return _level; } - set { _level = value; } + } - /// - /// Gets or sets the to provide loggers. - /// - public static ILogManager Instance - { - get { return _manager; } - set { _manager = value ?? NopLogManager.Instance; } - } + public static IServiceProvider Provider { get; set; } /// /// Gets a logger for the given type. /// - public static ILogger GetLogger(Type type) + public static ILogger GetLogger() { - return _manager.GetLogger(type); + return (ILogger)Provider?.GetService(typeof(ILogger)); } - /// - /// Gets a logger for the given type name. - /// - public static ILogger GetLogger(String name) + public static ILogger GetLogger() { - return _manager.GetLogger(name); + return (ILogger)Provider?.GetService(typeof(ILogger)); } } - - /// - /// Log levels. - /// - public enum LogLevel - { - /// - /// All logs. - /// - All, - /// - /// Debugs and above. - /// - Debug, - /// - /// Infos and above. - /// - Info, - /// - /// Warnings and above. - /// - Warning, - /// - /// Errors and above. - /// - Error, - /// - /// Fatals only. - /// - Fatal, - /// - /// No logs. - /// - None - } } diff --git a/WorldDirect.CoAP/Log/NopLogManager.cs b/WorldDirect.CoAP/Log/NopLogManager.cs deleted file mode 100644 index fb5bc9c..0000000 --- a/WorldDirect.CoAP/Log/NopLogManager.cs +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2011-2014, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - - -namespace WorldDirect.CoAP.Log -{ - using System; - - /// - /// A which always returns the unique instance of - /// a direct NOP (no operation) logger. - /// - public sealed class NopLogManager : ILogManager - { - /// - /// The singleton instance. - /// - public static readonly NopLogManager Instance = new NopLogManager(); - private static readonly NopLogger NOP = new NopLogger(); - - private NopLogManager() - { } - - /// - public ILogger GetLogger(Type type) - { - return NOP; - } - - /// - public ILogger GetLogger(String name) - { - return NOP; - } - - class NopLogger : ILogger - { - public Boolean IsDebugEnabled - { - get { return false; } - } - - public Boolean IsErrorEnabled - { - get { return false; } - } - - public Boolean IsFatalEnabled - { - get { return false; } - } - - public Boolean IsInfoEnabled - { - get { return false; } - } - - public Boolean IsWarnEnabled - { - get { return false; } - } - - public void Debug(Object message) - { - // NOP - } - - public void Debug(Object message, Exception exception) - { - // NOP - } - - public void Error(Object message) - { - // NOP - } - - public void Error(Object message, Exception exception) - { - // NOP - } - - public void Fatal(Object message) - { - // NOP - } - - public void Fatal(Object message, Exception exception) - { - // NOP - } - - public void Info(Object message) - { - // NOP - } - - public void Info(Object message, Exception exception) - { - // NOP - } - - public void Warn(Object message) - { - // NOP - } - - public void Warn(Object message, Exception exception) - { - // NOP - } - } - } -} diff --git a/WorldDirect.CoAP/Log/TextWriterLogger.cs b/WorldDirect.CoAP/Log/TextWriterLogger.cs deleted file mode 100644 index 8edfdf4..0000000 --- a/WorldDirect.CoAP/Log/TextWriterLogger.cs +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2011-2014, Longxiang He , - * SmeshLink Technology Co. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY. - * - * This file is part of the CoAP.NET, a CoAP framework in C#. - * Please see README for more information. - */ - -namespace WorldDirect.CoAP.Log -{ - using System; - - /// - /// Logger that writes logs to a . - /// - public class TextWriterLogger : ILogger - { - private System.IO.TextWriter _writer; - - /// - /// Instantiates. - /// - public TextWriterLogger(System.IO.TextWriter writer) - { - _writer = writer; - } - - /// - public Boolean IsDebugEnabled - { - get { return LogLevel.Debug >= LogManager.Level; } - } - - /// - public Boolean IsInfoEnabled - { - get { return LogLevel.Info >= LogManager.Level; } - } - - /// - public Boolean IsErrorEnabled - { - get { return LogLevel.Error >= LogManager.Level; } - } - - /// - public Boolean IsFatalEnabled - { - get { return LogLevel.Fatal >= LogManager.Level; } - } - - /// - public Boolean IsWarnEnabled - { - get { return LogLevel.Warning >= LogManager.Level; } - } - - /// - public void Error(Object sender, String msg, params Object[] args) - { - String format = String.Format("ERROR - {0}\n", msg); - if (sender != null) - { - format = "[" + sender.GetType().Name + "] " + format; - } - - _writer.Write(format, args); - } - - /// - public void Warning(Object sender, String msg, params Object[] args) - { - String format = String.Format("WARNING - {0}\n", msg); - if (sender != null) - { - format = "[" + sender.GetType().Name + "] " + format; - } - - _writer.Write(format, args); - } - - /// - public void Info(Object sender, String msg, params Object[] args) - { - String format = String.Format("INFO - {0}\n", msg); - if (sender != null) - { - format = "[" + sender.GetType().Name + "] " + format; - } - - _writer.Write(format, args); - } - - /// - public void Debug(Object sender, String msg, params Object[] args) - { - String format = String.Format("DEBUG - {0}\n", msg); - if (sender != null) - { - format = "[" + sender.GetType().Name + "] " + format; - } - - _writer.Write(format, args); - } - - /// - public void Debug(Object message) - { - Log("DEBUG", message, null); - } - - /// - public void Debug(Object message, Exception exception) - { - Log("DEBUG", message, exception); - } - - /// - public void Error(Object message) - { - Log("Error", message, null); - } - - /// - public void Error(Object message, Exception exception) - { - Log("Error", message, exception); - } - - /// - public void Fatal(Object message) - { - Log("Fatal", message, null); - } - - /// - public void Fatal(Object message, Exception exception) - { - Log("Fatal", message, exception); - } - - /// - public void Info(Object message) - { - Log("Info", message, null); - } - - /// - public void Info(Object message, Exception exception) - { - Log("Info", message, exception); - } - - /// - public void Warn(Object message) - { - Log("Warn", message, null); - } - - /// - public void Warn(Object message, Exception exception) - { - Log("Warn", message, exception); - } - - private void Log(String level, Object message, Exception exception) - { - _writer.Write(level); - _writer.Write(" - "); - _writer.WriteLine(message); - if (exception != null) - _writer.WriteLine(exception); - } - } -} diff --git a/WorldDirect.CoAP/Message.cs b/WorldDirect.CoAP/Message.cs index 7a252c0..776a468 100644 --- a/WorldDirect.CoAP/Message.cs +++ b/WorldDirect.CoAP/Message.cs @@ -447,7 +447,7 @@ public override String ToString() payload += "... " + PayloadSize + " bytes"; } - return String.Format("{0}-{1} ID={2}, Token={3}, Options=[{4}], {5}", + return String.Format("({0}-{1}) MessageID={2}, Token={3}, Options=[{4}], {5}", Type, CoAP.Code.ToString(_code), ID, TokenString, Utils.OptionsToString(this), payload); } diff --git a/WorldDirect.CoAP/Metrics.cs b/WorldDirect.CoAP/Metrics.cs new file mode 100644 index 0000000..1edd603 --- /dev/null +++ b/WorldDirect.CoAP/Metrics.cs @@ -0,0 +1,69 @@ +namespace WorldDirect.CoAP +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Tracing; + using System.Text; + using System.Threading; + + [EventSource(Name = "WorldDirect.CoAP")] + internal sealed class Metrics : EventSource + { + /// + /// The provider to collect CoAP metrics. + /// + public static readonly Metrics Log = new Metrics(); + + private IncrementingPollingCounter sendingBytesRate; + private IncrementingPollingCounter receivedBytesRate; + private long totalTransmittedBytes; + private long totalReceivedBytes; + + public void BytesTransmitted(int bytes) + { + Interlocked.Add(ref this.totalTransmittedBytes, bytes); + } + + public void BytesReceived(int bytes) + { + Interlocked.Add(ref this.totalReceivedBytes, bytes); + } + + /// + /// Releases the unmanaged resources used by the class and optionally releases the managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + this.sendingBytesRate?.Dispose(); + this.sendingBytesRate = null; + + this.receivedBytesRate?.Dispose(); + this.receivedBytesRate = null; + + base.Dispose(disposing); + } + + /// + /// Called when the current event source is updated by the controller. + /// + /// The arguments for the event. + protected override void OnEventCommand(EventCommandEventArgs command) + { + if (command.Command == EventCommand.Enable) + { + this.sendingBytesRate ??= new IncrementingPollingCounter("udp-sent-bytes-rate", this, () => Volatile.Read(ref this.totalTransmittedBytes)) + { + DisplayName = "UDP Sent bytes", + DisplayUnits = "bytes/s", + }; + + this.receivedBytesRate ??= new IncrementingPollingCounter("udp-received-bytes-rate", this, () => Volatile.Read(ref this.totalReceivedBytes)) + { + DisplayName = "UDP Received bytes", + DisplayUnits = "bytes/s", + }; + } + } + } +} diff --git a/WorldDirect.CoAP/Net/CoAPEndPoint.cs b/WorldDirect.CoAP/Net/CoAPEndPoint.cs index 7f1e017..d33ccfc 100644 --- a/WorldDirect.CoAP/Net/CoAPEndPoint.cs +++ b/WorldDirect.CoAP/Net/CoAPEndPoint.cs @@ -16,6 +16,7 @@ namespace WorldDirect.CoAP.Net using Channel; using Codec; using Log; + using Microsoft.Extensions.Logging; using Stack; using Threading; @@ -24,7 +25,7 @@ namespace WorldDirect.CoAP.Net /// public partial class CoAPEndPoint : IEndPoint, IOutbox { - static readonly ILogger log = LogManager.GetLogger(typeof(CoAPEndPoint)); + static readonly ILogger log = LogManager.GetLogger(); readonly ICoapConfig _config; readonly IChannel _channel; @@ -35,6 +36,8 @@ public partial class CoAPEndPoint : IEndPoint, IOutbox private System.Net.EndPoint _localEP; private IExecutor _executor; + public string Scheme => CoapConstants.UriScheme; + /// public event EventHandler> SendingRequest; /// @@ -170,13 +173,11 @@ public void Start() } catch { - if (log.IsWarnEnabled) - log.Warn("Cannot start endpoint at " + _localEP); + log.LogWarning("Cannot start endpoint at " + _localEP); Stop(); throw; } - if (log.IsDebugEnabled) - log.Debug("Starting endpoint bound to " + _localEP); + log.LogDebug("Starting endpoint bound to " + _localEP); } /// @@ -184,8 +185,7 @@ public void Stop() { if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) return; - if (log.IsDebugEnabled) - log.Debug("Stopping endpoint bound to " + _localEP); + log.LogDebug("Stopping endpoint bound to " + _localEP); _channel.Stop(); _matcher.Stop(); _matcher.Clear(); @@ -240,13 +240,13 @@ private void ReceiveData(DataReceivedEventArgs e) try { request = decoder.DecodeRequest(); + request.EndPoint = this; } catch (Exception) { if (decoder.IsReply) { - if (log.IsWarnEnabled) - log.Warn("Message format error caused by " + e.EndPoint); + log.LogWarning("Message format error caused by " + e.EndPoint); } else { @@ -259,8 +259,7 @@ private void ReceiveData(DataReceivedEventArgs e) _channel.Send(Serialize(rst), rst.Destination); - if (log.IsWarnEnabled) - log.Warn("Message format error caused by " + e.EndPoint + " and reseted."); + log.LogWarning("Message format error caused by " + e.EndPoint + " and reseted."); } return; } @@ -297,8 +296,7 @@ private void ReceiveData(DataReceivedEventArgs e) } else if (response.Type != MessageType.ACK) { - if (log.IsDebugEnabled) - log.Debug("Rejecting unmatchable response from " + e.EndPoint); + log.LogDebug("Rejecting unmatchable response from " + e.EndPoint); Reject(response); } } @@ -315,8 +313,7 @@ private void ReceiveData(DataReceivedEventArgs e) // CoAP Ping if (message.Type == MessageType.CON || message.Type == MessageType.NON) { - if (log.IsDebugEnabled) - log.Debug("Responding to ping by " + e.EndPoint); + log.LogDebug("Responding to ping by " + e.EndPoint); Reject(message); } else @@ -330,9 +327,9 @@ private void ReceiveData(DataReceivedEventArgs e) } } } - else if (log.IsDebugEnabled) + else { - log.Debug("Silently ignoring non-CoAP message from " + e.EndPoint); + log.LogDebug("Silently ignoring non-CoAP message from " + e.EndPoint); } } diff --git a/WorldDirect.CoAP/Net/Exchange.cs b/WorldDirect.CoAP/Net/Exchange.cs index b3cf9ce..ed3d7eb 100644 --- a/WorldDirect.CoAP/Net/Exchange.cs +++ b/WorldDirect.CoAP/Net/Exchange.cs @@ -13,6 +13,7 @@ namespace WorldDirect.CoAP.Net { using System; using System.Collections.Concurrent; + using System.Diagnostics; using Observe; using Stack; using Util; @@ -270,7 +271,14 @@ public KeyID(Int32 id, System.Net.EndPoint ep) { _id = id; _endpoint = ep; - _hash = id * 31 + (ep == null ? 0 : ep.GetHashCode()); + if (ep == null) + { + _hash = id * 31; + } + else + { + _hash = HashCode.Combine(id, _endpoint); + } } /// diff --git a/WorldDirect.CoAP/Net/IEndPoint.cs b/WorldDirect.CoAP/Net/IEndPoint.cs index 78690f9..f727303 100644 --- a/WorldDirect.CoAP/Net/IEndPoint.cs +++ b/WorldDirect.CoAP/Net/IEndPoint.cs @@ -40,6 +40,10 @@ public interface IEndPoint : IDisposable /// IOutbox Outbox { get; } /// + /// Gets the scheme. + /// + string Scheme { get; } + /// /// Occurs when a request is about to be sent. /// event EventHandler> SendingRequest; diff --git a/WorldDirect.CoAP/Net/Matcher.cs b/WorldDirect.CoAP/Net/Matcher.cs index 1fd22b2..fe55b9f 100644 --- a/WorldDirect.CoAP/Net/Matcher.cs +++ b/WorldDirect.CoAP/Net/Matcher.cs @@ -14,14 +14,16 @@ namespace WorldDirect.CoAP.Net using System; using System.Collections.Concurrent; using System.Collections.Generic; + using System.ComponentModel; using Deduplication; using Log; + using Microsoft.Extensions.Logging; using Observe; using Util; - class Matcher : IMatcher, IDisposable + public class Matcher : IMatcher, IDisposable { - static readonly ILogger log = LogManager.GetLogger(typeof(Matcher)); + static readonly ILogger log = LogManager.GetLogger(); /// /// for all @@ -39,14 +41,15 @@ class Matcher : IMatcher, IDisposable readonly ConcurrentDictionary _ongoingExchanges = new ConcurrentDictionary(); private Int32 _running; - private Int32 _currentID; + private readonly MessageIdProvider currentIdProvider; private IDeduplicator _deduplicator; public Matcher(ICoapConfig config) { _deduplicator = DeduplicatorFactory.CreateDeduplicator(config); - if (config.UseRandomIDStart) - _currentID = new Random().Next(1 << 16); + this.currentIdProvider = new MessageIdProvider(config); + //if (config.UseRandomIDStart) + //_currentID = new Random().Next(1 << 16); } /// @@ -55,6 +58,7 @@ public void Start() if (System.Threading.Interlocked.CompareExchange(ref _running, 1, 0) > 0) return; _deduplicator.Start(); + this.currentIdProvider.Start(); } /// @@ -63,6 +67,7 @@ public void Stop() if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) return; _deduplicator.Stop(); + this.currentIdProvider.Stop(); Clear(); } @@ -79,8 +84,9 @@ public void Clear() public void SendRequest(Exchange exchange, Request request) { if (request.ID == Message.None) - request.ID = System.Threading.Interlocked.Increment(ref _currentID) % (1 << 16); + request.ID = this.currentIdProvider.Get(request.Destination); + log.LogTrace("Send request with {MessageId} to {Remote}", request.ID, request.Destination); /* * The request is a CON or NON and must be prepared for these responses * - CON => ACK / RST / ACK+response / CON+response / NON+response @@ -89,13 +95,12 @@ public void SendRequest(Exchange exchange, Request request) */ // the MID is from the local namespace -- use blank address - Exchange.KeyID keyID = new Exchange.KeyID(request.ID, null); + Exchange.KeyID keyID = new Exchange.KeyID(request.ID, request.Destination); Exchange.KeyToken keyToken = new Exchange.KeyToken(request.Token); exchange.Completed += OnExchangeCompleted; - if (log.IsDebugEnabled) - log.Debug("Stored open request by " + keyID + ", " + keyToken); + log.LogTrace("Stored open request by " + keyID + ", " + keyToken); _exchangesByID[keyID] = exchange; _exchangesByToken[keyToken] = exchange; @@ -105,8 +110,9 @@ public void SendRequest(Exchange exchange, Request request) public void SendResponse(Exchange exchange, Response response) { if (response.ID == Message.None) - response.ID = System.Threading.Interlocked.Increment(ref _currentID) % (1 << 16); + response.ID = this.currentIdProvider.Get(exchange.Request.Source); + log.LogTrace("Send response with {MessageId} to {Remote}", response.ID, response.Destination); /* * The response is a CON or NON or ACK and must be prepared for these * - CON => ACK / RST // we only care to stop retransmission @@ -138,19 +144,16 @@ public void SendResponse(Exchange exchange, Response response) // Remember ongoing blockwise GET requests if (Utils.Put(_ongoingExchanges, keyUri, exchange) == null) { - if (log.IsDebugEnabled) - log.Debug("Ongoing Block2 started late, storing " + keyUri + " for " + request); + log.LogDebug("Ongoing Block2 started late, storing " + keyUri + " for " + request); } else { - if (log.IsDebugEnabled) - log.Debug("Ongoing Block2 continued, storing " + keyUri + " for " + request); + log.LogDebug("Ongoing Block2 continued, storing " + keyUri + " for " + request); } } else { - if (log.IsDebugEnabled) - log.Debug("Ongoing Block2 completed, cleaning up " + keyUri + " for " + request); + log.LogDebug("Ongoing Block2 completed, cleaning up " + keyUri + " for " + request); Exchange exc; _ongoingExchanges.TryRemove(keyUri, out exc); } @@ -197,6 +200,7 @@ public Exchange ReceiveRequest(Request request) */ Exchange.KeyID keyId = new Exchange.KeyID(request.ID, request.Source); + log.LogTrace("Received request with {MessageId} from {Remote}", request.ID, request.Source); /* * The differentiation between the case where there is a Block1 or @@ -215,8 +219,7 @@ public Exchange ReceiveRequest(Request request) } else { - if (log.IsInfoEnabled) - log.Info("Duplicate request: " + request); + log.LogDebug("Duplicate request: {MessageId} from {Remote}", request.ID, request.Source); request.Duplicate = true; return previous; } @@ -225,8 +228,7 @@ public Exchange ReceiveRequest(Request request) { Exchange.KeyUri keyUri = new Exchange.KeyUri(request.URI, request.Source); - if (log.IsDebugEnabled) - log.Debug("Looking up ongoing exchange for " + keyUri); + log.LogDebug("Looking up ongoing exchange for " + keyUri); Exchange ongoing; if (_ongoingExchanges.TryGetValue(keyUri, out ongoing)) @@ -234,8 +236,7 @@ public Exchange ReceiveRequest(Request request) Exchange prev = _deduplicator.FindPrevious(keyId, ongoing); if (prev != null) { - if (log.IsInfoEnabled) - log.Info("Duplicate ongoing request: " + request); + log.LogInformation("Duplicate ongoing request: " + request); request.Duplicate = true; } else @@ -244,8 +245,7 @@ public Exchange ReceiveRequest(Request request) if (ongoing.CurrentResponse.Type != MessageType.ACK && !ongoing.CurrentResponse.HasOption(OptionType.Observe)) { keyId = new Exchange.KeyID(ongoing.CurrentResponse.ID, null); - if (log.IsDebugEnabled) - log.Debug("Ongoing exchange got new request, cleaning up " + keyId); + log.LogDebug("Ongoing exchange got new request, cleaning up " + keyId); _exchangesByID.Remove(keyId); } } @@ -266,16 +266,14 @@ public Exchange ReceiveRequest(Request request) Exchange previous = _deduplicator.FindPrevious(keyId, exchange); if (previous == null) { - if (log.IsDebugEnabled) - log.Debug("New ongoing request, storing " + keyUri + " for " + request); + log.LogDebug("New ongoing request, storing " + keyUri + " for " + request); exchange.Completed += OnExchangeCompleted; _ongoingExchanges[keyUri] = exchange; return exchange; } else { - if (log.IsInfoEnabled) - log.Info("Duplicate initial request: " + request); + log.LogInformation("Duplicate initial request: " + request); request.Duplicate = true; return previous; } @@ -293,14 +291,8 @@ public Exchange ReceiveResponse(Response response) * => resend ACK */ - Exchange.KeyID keyId; - if (response.Type == MessageType.ACK) - // own namespace - keyId = new Exchange.KeyID(response.ID, null); - else - // remote namespace - keyId = new Exchange.KeyID(response.ID, response.Source); - + Exchange.KeyID keyId = new Exchange.KeyID(response.ID, response.Source); + log.LogTrace("Received response with {MessageId} from {Remote}", response.ID, response.Source); Exchange.KeyToken keyToken = new Exchange.KeyToken(response.Token); Exchange exchange; @@ -311,23 +303,20 @@ public Exchange ReceiveResponse(Response response) if (prev != null) { // (and thus it holds: prev == exchange) - if (log.IsInfoEnabled) - log.Info("Duplicate response for open exchange: " + response); + log.LogDebug("Duplicate response for open exchange: {response} from {Remote}. Started request at {RequestTimestamp}", response, response.Source, exchange.Timestamp); response.Duplicate = true; } else { - keyId = new Exchange.KeyID(exchange.CurrentRequest.ID, null); - if (log.IsDebugEnabled) - log.Debug("Exchange got response: Cleaning up " + keyId); + keyId = new Exchange.KeyID(exchange.CurrentRequest.ID, response.Source); + log.LogTrace("Exchange got response: Cleaning up " + keyId); _exchangesByID.Remove(keyId); } if (response.Type == MessageType.ACK && exchange.CurrentRequest.ID != response.ID) { // The token matches but not the MID. This is a response for an older exchange - if (log.IsWarnEnabled) - log.Warn("Possible MID reuse before lifetime end: " + response.TokenString + " expected MID " + exchange.CurrentRequest.ID + " but received " + response.ID); + log.LogWarning("Possible MID reuse before lifetime end: " + response.TokenString + " expected MID " + exchange.CurrentRequest.ID + " but received " + response.ID); } return exchange; @@ -341,16 +330,14 @@ public Exchange ReceiveResponse(Response response) Exchange prev = _deduplicator.Find(keyId); if (prev != null) { - if (log.IsInfoEnabled) - log.Info("Duplicate response for completed exchange: " + response); + log.LogInformation("Duplicate response for completed exchange: " + response); response.Duplicate = true; return prev; } } else { - if (log.IsInfoEnabled) - log.Info("Ignoring unmatchable piggy-backed response from " + response.Source + ": " + response); + log.LogTrace("Ignoring unmatchable piggy-backed response from " + response.Source + ": " + response); } // ignore response return null; @@ -365,15 +352,13 @@ public Exchange ReceiveEmptyMessage(EmptyMessage message) Exchange exchange; if (_exchangesByID.TryGetValue(keyID, out exchange)) { - if (log.IsDebugEnabled) - log.Debug("Exchange got reply: Cleaning up " + keyID); + log.LogDebug("Exchange got reply: Cleaning up " + keyID); _exchangesByID.Remove(keyID); return exchange; } else { - if (log.IsInfoEnabled) - log.Info("Ignoring unmatchable empty message from " + message.Source + ": " + message); + log.LogInformation("Ignoring unmatchable empty message from " + message.Source + ": " + message); return null; } } @@ -384,12 +369,12 @@ public void Dispose() IDisposable d = _deduplicator as IDisposable; if (d != null) d.Dispose(); + this.currentIdProvider.Dispose(); } private void RemoveNotificatoinsOf(ObserveRelation relation) { - if (log.IsDebugEnabled) - log.Debug("Remove all remaining NON-notifications of observe relation"); + log.LogDebug("Remove all remaining NON-notifications of observe relation"); foreach (Response previous in relation.ClearNotifications()) { @@ -411,11 +396,10 @@ private void OnExchangeCompleted(Object sender, EventArgs e) if (exchange.Origin == Origin.Local) { // this endpoint created the Exchange by issuing a request - Exchange.KeyID keyID = new Exchange.KeyID(exchange.CurrentRequest.ID, null); + Exchange.KeyID keyID = new Exchange.KeyID(exchange.CurrentRequest.ID, exchange.Request.Destination); Exchange.KeyToken keyToken = new Exchange.KeyToken(exchange.CurrentRequest.Token); - if (log.IsDebugEnabled) - log.Debug("Exchange completed: Cleaning up " + keyToken); + log.LogTrace("Exchange completed: Cleaning up " + keyToken); _exchangesByToken.Remove(keyToken); // in case an empty ACK was lost @@ -439,8 +423,7 @@ private void OnExchangeCompleted(Object sender, EventArgs e) if (request != null && (request.HasOption(OptionType.Block1) || response != null && response.HasOption(OptionType.Block2))) { Exchange.KeyUri uriKey = new Exchange.KeyUri(request.URI, request.Source); - if (log.IsDebugEnabled) - log.Debug("Remote ongoing completed, cleaning up " + uriKey); + log.LogDebug("Remote ongoing completed, cleaning up " + uriKey); Exchange exc; _ongoingExchanges.TryRemove(uriKey, out exc); } diff --git a/WorldDirect.CoAP/Net/MessageIdProvider.cs b/WorldDirect.CoAP/Net/MessageIdProvider.cs new file mode 100644 index 0000000..6b19861 --- /dev/null +++ b/WorldDirect.CoAP/Net/MessageIdProvider.cs @@ -0,0 +1,86 @@ +namespace WorldDirect.CoAP.Net +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Net; + using System.Text; + using System.Threading; + using System.Transactions; + using Log; + using Microsoft.Extensions.Logging; + + internal struct MessageIdState + { + + public DateTime LastUsed { get; set; } + public int Id { get; set; } + + public void Inc() + { + this.Id = (this.Id + 1) % (1 << 16); + this.LastUsed = DateTime.Now; + } + + public static MessageIdState Create() + { + return new MessageIdState() {Id = new Random((int)DateTimeOffset.Now.Ticks).Next() % (1 << 16), LastUsed = DateTime.Now,}; + } + } + + public class MessageIdProvider : IDisposable + { + private Timer _timer; + private ICoapConfig _config; + private ConcurrentDictionary state = new ConcurrentDictionary(); + + public MessageIdProvider(ICoapConfig config) + { + this._config = config; + } + + public int Get(EndPoint ep) + { + var cur = this.state.AddOrUpdate(ep, (_) => MessageIdState.Create(), (endpoint, current) => + { + current.Inc(); + return current; + }); + + return cur.Id; + } + + public void Start() + { + _timer = new Timer(Clean, null, TimeSpan.FromMilliseconds(_config.MarkAndSweepInterval), TimeSpan.FromMilliseconds(_config.MarkAndSweepInterval)); + } + + public void Stop() + { + Dispose(); + Clear(); + } + + public void Dispose() + { + _timer?.Dispose(); + } + + private void Clean(object _) + { + DateTime oldestAllowed = DateTime.Now.AddMilliseconds(-_config.ExchangeLifetime); + foreach (var kvp in this.state) + { + if (kvp.Value.LastUsed < oldestAllowed) + { + this.state.TryRemove(kvp.Key, out var _); + } + } + } + + private void Clear() + { + this.state.Clear(); + } + } +} diff --git a/WorldDirect.CoAP/Observe/ObserveRelation.cs b/WorldDirect.CoAP/Observe/ObserveRelation.cs index 4f665a9..e5861ea 100644 --- a/WorldDirect.CoAP/Observe/ObserveRelation.cs +++ b/WorldDirect.CoAP/Observe/ObserveRelation.cs @@ -15,6 +15,7 @@ namespace WorldDirect.CoAP.Observe using System.Collections.Concurrent; using System.Collections.Generic; using Log; + using Microsoft.Extensions.Logging; using Net; using Server.Resources; using Util; @@ -24,7 +25,7 @@ namespace WorldDirect.CoAP.Observe /// public class ObserveRelation { - static readonly ILogger log = LogManager.GetLogger(typeof(ObserveRelation)); + static readonly ILogger log = LogManager.GetLogger(); readonly ICoapConfig _config; readonly ObservingEndpoint _endpoint; readonly IResource _resource; @@ -120,8 +121,7 @@ public Boolean Established /// public void Cancel() { - if (log.IsDebugEnabled) - log.Debug("Cancel observe relation from " + _key + " with " + _resource.Path); + log.LogDebug("Cancel observe relation from " + _key + " with " + _resource.Path); // stop ongoing retransmissions if (_exchange.Response != null) _exchange.Response.Cancel(); diff --git a/WorldDirect.CoAP/Request.cs b/WorldDirect.CoAP/Request.cs index 955418b..f9b029f 100644 --- a/WorldDirect.CoAP/Request.cs +++ b/WorldDirect.CoAP/Request.cs @@ -95,7 +95,7 @@ public Uri URI if (_uri == null) { UriBuilder ub = new UriBuilder(); - ub.Scheme = CoapConstants.UriScheme; + ub.Scheme = this.EndPoint.Scheme; ub.Host = UriHost ?? "localhost"; ub.Port = UriPort; ub.Path = UriPath; diff --git a/WorldDirect.CoAP/Server/CoapServer.cs b/WorldDirect.CoAP/Server/CoapServer.cs index 6645210..e4871d6 100644 --- a/WorldDirect.CoAP/Server/CoapServer.cs +++ b/WorldDirect.CoAP/Server/CoapServer.cs @@ -15,6 +15,7 @@ namespace WorldDirect.CoAP.Server using System.Collections.Generic; using System.Net; using Log; + using Microsoft.Extensions.Logging; using Net; using Resources; @@ -23,7 +24,7 @@ namespace WorldDirect.CoAP.Server /// public class CoapServer : IServer { - static readonly ILogger log = LogManager.GetLogger(typeof(CoapServer)); + static readonly ILogger log = LogManager.GetLogger(); readonly IResource _root; readonly List _endpoints = new List(); readonly ICoapConfig _config; @@ -162,8 +163,7 @@ public Boolean Remove(IResource resource) /// public void Start() { - if (log.IsDebugEnabled) - log.Debug("Starting CoAP server"); + log.LogDebug("Starting CoAP server"); if (_endpoints.Count == 0) { @@ -180,8 +180,7 @@ public void Start() } catch (Exception e) { - if (log.IsWarnEnabled) - log.Warn("Could not start endpoint " + endpoint.LocalEndPoint, e); + log.LogWarning("Could not start endpoint " + endpoint.LocalEndPoint, e); } } @@ -192,8 +191,7 @@ public void Start() /// public void Stop() { - if (log.IsDebugEnabled) - log.Debug("Starting CoAP server"); + log.LogDebug("Starting CoAP server"); _endpoints.ForEach(ep => ep.Stop()); } diff --git a/WorldDirect.CoAP/Server/Resources/AsyncResource.cs b/WorldDirect.CoAP/Server/Resources/AsyncResource.cs new file mode 100644 index 0000000..4cff538 --- /dev/null +++ b/WorldDirect.CoAP/Server/Resources/AsyncResource.cs @@ -0,0 +1,172 @@ +namespace WorldDirect.CoAP.Server.Resources +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Log; + using Microsoft.Extensions.Logging; + + public class AsyncResource: Server.Resources.Resource, IDisposable + { + private ConcurrentDictionary _tasks = new ConcurrentDictionary(); + private CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly ILogger _logger; + private bool _disposed = false; + + public AsyncResource(string name) : base(name) + { + this._logger = LogManager.GetLogger(); + } + + public AsyncResource(string name, bool visible) : base(name, visible) + { + this._logger = LogManager.GetLogger(); + } + + protected sealed override void DoGet(CoapExchange exchange) + { + var guid = Guid.NewGuid(); + var t = this.GetAsync(exchange, guid, this._cts.Token); + this._tasks.AddOrUpdate(guid, t, (g, _) => t); + } + + protected sealed override void DoPost(CoapExchange exchange) + { + var guid = Guid.NewGuid(); + var t = this.PostAsync(exchange, guid, this._cts.Token); + this._tasks.AddOrUpdate(guid, t, (g, _) => t); + } + + protected sealed override void DoPut(CoapExchange exchange) + { + var guid = Guid.NewGuid(); + var t = this.PutAsync(exchange, guid, this._cts.Token); + this._tasks.AddOrUpdate(guid, t, (g, _) => t); + } + + protected sealed override void DoDelete(CoapExchange exchange) + { + var guid = Guid.NewGuid(); + var t = this.DeleteAsync(exchange, guid, this._cts.Token); + this._tasks.AddOrUpdate(guid, t, (g, _) => t); + } + + + + private async Task GetAsync(CoapExchange exchange, Guid taskId, CancellationToken ct) + { + try + { + await this.DoGetAsync(exchange, ct).ConfigureAwait(false); + } + catch (Exception e) + { + this._logger.LogError(e, "Unhandled exception in GET {Resource}", this.Name); + } + finally + { + this._tasks.TryRemove(taskId, out _); + } + } + + private async Task PostAsync(CoapExchange exchange, Guid taskId, CancellationToken ct) + { + try + { + await this.DoPostAsync(exchange, ct).ConfigureAwait(false); + } + catch (Exception e) + { + this._logger.LogError(e, "Unhandled exception in POST {Resource}", this.Name); + } + finally + { + this._tasks.TryRemove(taskId, out _); + } + } + + private async Task PutAsync(CoapExchange exchange, Guid taskId, CancellationToken ct) + { + try + { + await this.DoPutAsync(exchange, ct).ConfigureAwait(false); + } + catch (Exception e) + { + this._logger.LogError(e, "Unhandled exception in PUT {Resource}", this.Name); + } + finally + { + this._tasks.TryRemove(taskId, out _); + } + } + + private async Task DeleteAsync(CoapExchange exchange, Guid taskId, CancellationToken ct) + { + try + { + await this.DoDeleteAsync(exchange, ct).ConfigureAwait(false); + } + catch (Exception e) + { + this._logger.LogError(e, "Unhandled exception in DELETE {Resource}", this.Name); + } + finally + { + this._tasks.TryRemove(taskId, out _); + } + } + + protected virtual Task DoGetAsync(CoapExchange exchange, CancellationToken ct) + { + exchange.Respond(StatusCode.MethodNotAllowed); + return Task.CompletedTask; + } + + + protected virtual Task DoPostAsync(CoapExchange exchange, CancellationToken ct) + { + exchange.Respond(StatusCode.MethodNotAllowed); + return Task.CompletedTask; + } + + protected virtual Task DoPutAsync(CoapExchange exchange, CancellationToken ct) + { + exchange.Respond(StatusCode.MethodNotAllowed); + return Task.CompletedTask; + } + + protected virtual Task DoDeleteAsync(CoapExchange exchange, CancellationToken ct) + { + exchange.Respond(StatusCode.MethodNotAllowed); + return Task.CompletedTask; + } + + public void Dispose() + { + // Dispose of unmanaged resources. + Dispose(true); + // Suppress finalization. + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _cts?.Cancel(); + _cts?.Dispose(); + } + + _disposed = true; + } + } +} diff --git a/WorldDirect.CoAP/Server/Resources/CoapExchange.cs b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs index 6cdbf6a..20301d8 100644 --- a/WorldDirect.CoAP/Server/Resources/CoapExchange.cs +++ b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs @@ -12,6 +12,7 @@ namespace WorldDirect.CoAP.Server.Resources { using System; + using System.Diagnostics; using Net; /// @@ -23,6 +24,7 @@ public class CoapExchange { readonly Exchange _exchange; readonly Resource _resource; + private Activity activity; private String _locationPath; private String _locationQuery; @@ -37,6 +39,19 @@ internal CoapExchange(Exchange exchange, Resource resource) { _exchange = exchange; _resource = resource; + Activity.Current = null; + var request = exchange.Request; + this.activity = Tracing.ServerSource.CreateActivity($"CoAP {request.UriPath}", ActivityKind.Server); + this.activity?.Start(); + this.activity?.AddTag("coap.method", request.Method); + this.activity?.AddTag("coap.uri", request.URI); + this.activity?.AddTag("coap.resource", request.UriPath); + this.activity?.AddTag("coap.remote", request.Source); + } + + public T Get(Object key) + { + return this._exchange.Get(key); } /// @@ -175,8 +190,11 @@ public void Respond(Response response) response.SetOption(Option.Create(OptionType.ETag, _eTag)); _resource.CheckObserveRelation(_exchange, response); - + _exchange.SendResponse(response); + this.activity?.AddTag("coap.statuscode", response.StatusCode); + this.activity?.Stop(); + this.activity = null; } } } diff --git a/WorldDirect.CoAP/Server/Resources/Resource.cs b/WorldDirect.CoAP/Server/Resources/Resource.cs index 7cfb955..d22e6d2 100644 --- a/WorldDirect.CoAP/Server/Resources/Resource.cs +++ b/WorldDirect.CoAP/Server/Resources/Resource.cs @@ -15,6 +15,7 @@ namespace WorldDirect.CoAP.Server.Resources using System.Collections.Concurrent; using System.Collections.Generic; using Log; + using Microsoft.Extensions.Logging; using Net; using Observe; using Threading; @@ -26,7 +27,7 @@ namespace WorldDirect.CoAP.Server.Resources public class Resource : IResource { static readonly IEnumerable EmptyEndPoints = new IEndPoint[0]; - static readonly ILogger log = LogManager.GetLogger(typeof(Resource)); + static readonly ILogger log = LogManager.GetLogger(); readonly ResourceAttributes _attributes = new ResourceAttributes(); private String _name; private String _path = String.Empty; @@ -278,13 +279,11 @@ public void AddObserveRelation(ObserveRelation relation) if (old != null) { old.Cancel(); - if (log.IsDebugEnabled) - log.Debug("Replacing observe relation between " + relation.Key + " and resource " + Uri); + log.LogDebug("Replacing observe relation between " + relation.Key + " and resource " + Uri); } else { - if (log.IsDebugEnabled) - log.Debug("Successfully established observe relation between " + relation.Key + " and resource " + Uri); + log.LogDebug("Successfully established observe relation between " + relation.Key + " and resource " + Uri); } } diff --git a/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs b/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs index c2c75f3..45d45e0 100644 --- a/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs +++ b/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs @@ -13,7 +13,9 @@ namespace WorldDirect.CoAP.Server { using System; using System.Collections.Generic; + using System.Diagnostics; using Log; + using Microsoft.Extensions.Logging; using Net; using Observe; using Resources; @@ -26,7 +28,7 @@ namespace WorldDirect.CoAP.Server /// public class ServerMessageDeliverer : IMessageDeliverer { - static readonly ILogger log = LogManager.GetLogger(typeof(ServerMessageDeliverer)); + static readonly ILogger log = LogManager.GetLogger(); readonly ICoapConfig _config; readonly IResource _root; readonly ObserveManager _observeManager = new ObserveManager(); @@ -44,7 +46,10 @@ public ServerMessageDeliverer(ICoapConfig config, IResource root) /// public void DeliverRequest(Exchange exchange) { + Activity.Current = null; + Request request = exchange.Request; + IResource resource = FindResource(request.UriPaths); if (resource != null) { @@ -56,6 +61,7 @@ public void DeliverRequest(Exchange exchange) executor.Start(() => resource.HandleRequest(exchange)); else resource.HandleRequest(exchange); + } else { @@ -101,8 +107,7 @@ private void CheckForObserveOption(Exchange exchange, IResource resource) if (obs == 0) { // Requests wants to observe and resource allows it :-) - if (log.IsDebugEnabled) - log.Debug("Initiate an observe relation between " + source + " and resource " + resource.Uri); + log.LogDebug("Initiate an observe relation between " + source + " and resource " + resource.Uri); ObservingEndpoint remote = _observeManager.FindObservingEndpoint(source); ObserveRelation relation = new ObserveRelation(_config, remote, resource, exchange); remote.AddObserveRelation(relation); diff --git a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs index 9779993..a5b3f89 100644 --- a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs +++ b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs @@ -15,11 +15,12 @@ namespace WorldDirect.CoAP.Stack using System.Linq; using System.Threading; using Log; + using Microsoft.Extensions.Logging; using Net; public class BlockwiseLayer : AbstractLayer { - static readonly ILogger log = LogManager.GetLogger(typeof(BlockwiseLayer)); + static readonly ILogger log = LogManager.GetLogger(); private Int32 _maxMessageSize; private Int32 _defaultBlockSize; @@ -33,8 +34,7 @@ public BlockwiseLayer(ICoapConfig config) _maxMessageSize = config.MaxMessageSize; _defaultBlockSize = config.DefaultBlockSize; _blockTimeout = config.BlockwiseStatusLifetime; - if (log.IsDebugEnabled) - log.Debug("BlockwiseLayer uses MaxMessageSize: " + _maxMessageSize + " and DefaultBlockSize:" + _defaultBlockSize); + log.LogInformation("BlockwiseLayer uses MaxMessageSize: " + _maxMessageSize + " and DefaultBlockSize:" + _defaultBlockSize); config.PropertyChanged += ConfigChanged; } @@ -60,8 +60,7 @@ public override void SendRequest(INextLayer nextLayer, Exchange exchange, Reques // Note: We do not regard it as random access when the block num is // 0. This is because the user might just want to do early block // size negotiation but actually wants to receive all blocks. - if (log.IsDebugEnabled) - log.Debug("Request carries explicit defined block2 option: create random access blockwise status"); + log.LogTrace("Request carries explicit defined block2 option: create random access blockwise status"); BlockwiseStatus status = new BlockwiseStatus(request.ContentFormat); BlockOption block2 = request.Block2; status.CurrentSZX = block2.SZX; @@ -73,8 +72,7 @@ public override void SendRequest(INextLayer nextLayer, Exchange exchange, Reques else if (RequiresBlockwise(request)) { // This must be a large POST or PUT request - if (log.IsDebugEnabled) - log.Debug("Request payload " + request.PayloadSize + "/" + _maxMessageSize + " requires Blockwise."); + log.LogTrace("Request payload " + request.PayloadSize + "/" + _maxMessageSize + " requires Blockwise."); BlockwiseStatus status = FindRequestBlockStatus(exchange, request); Request block = GetNextRequestBlock(request, status); exchange.RequestBlockStatus = status; @@ -95,15 +93,13 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req { // This must be a large POST or PUT request BlockOption block1 = request.Block1; - if (log.IsDebugEnabled) - log.Debug("Request contains block1 option " + block1); + log.LogTrace("Request contains block1 option " + block1); BlockwiseStatus status = FindRequestBlockStatus(exchange, request); if (block1.NUM == 0 && status.CurrentNUM > 0) { // reset the blockwise transfer - if (log.IsDebugEnabled) - log.Debug("Block1 num is 0, the client has restarted the blockwise transfer. Reset status."); + log.LogTrace("Block1 num is 0, the client has restarted the blockwise transfer. Reset status."); status = new BlockwiseStatus(request.ContentType); exchange.RequestBlockStatus = status; } @@ -128,8 +124,7 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req status.CurrentNUM = status.CurrentNUM + 1; if (block1.M) { - if (log.IsDebugEnabled) - log.Debug("There are more blocks to come. Acknowledge this block."); + log.LogTrace("There are more blocks to come. Acknowledge this block."); Response piggybacked = Response.CreateResponse(request, StatusCode.Continue); piggybacked.AddOption(new BlockOption(OptionType.Block1, block1.NUM, block1.SZX, true)); @@ -142,8 +137,7 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req } else { - if (log.IsDebugEnabled) - log.Debug("This was the last block. Deliver request"); + log.LogTrace("This was the last block. Deliver request"); // Remember block to acknowledge. TODO: We might make this a boolean flag in status. exchange.Block1ToAck = block1; @@ -162,8 +156,7 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req else { // ERROR, wrong number, Incomplete - if (log.IsWarnEnabled) - log.Warn("Wrong block number. Expected " + status.CurrentNUM + " but received " + block1.NUM + ". Respond with 4.08 (Request Entity Incomplete)."); + log.LogWarning("Wrong block number. Expected " + status.CurrentNUM + " but received " + block1.NUM + ". Respond with 4.08 (Request Entity Incomplete)."); Response error = Response.CreateResponse(request, StatusCode.RequestEntityIncomplete); error.AddOption(new BlockOption(OptionType.Block1, block1.NUM, block1.SZX, block1.M)); error.SetPayload("Wrong block number"); @@ -188,15 +181,13 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req if (status.Complete) { // clean up blockwise status - if (log.IsDebugEnabled) - log.Debug("Ongoing is complete " + status); + log.LogTrace("Ongoing is complete " + status); exchange.ResponseBlockStatus = null; ClearBlockCleanup(exchange); } else { - if (log.IsDebugEnabled) - log.Debug("Ongoing is continuing " + status); + log.LogTrace("Ongoing is continuing " + status); } exchange.CurrentResponse = block; @@ -221,8 +212,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo if (RequiresBlockwise(exchange, response)) { - if (log.IsDebugEnabled) - log.Debug("Response payload " + response.PayloadSize + "/" + _maxMessageSize + " requires Blockwise"); + log.LogTrace("Response payload " + response.PayloadSize + "/" + _maxMessageSize + " requires Blockwise"); BlockwiseStatus status = FindResponseBlockStatus(exchange, response); @@ -236,15 +226,13 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo if (status.Complete) { // clean up blockwise status - if (log.IsDebugEnabled) - log.Debug("Ongoing finished on first block " + status); + log.LogTrace("Ongoing finished on first block " + status); exchange.ResponseBlockStatus = null; ClearBlockCleanup(exchange); } else { - if (log.IsDebugEnabled) - log.Debug("Ongoing started " + status); + log.LogTrace("Ongoing started " + status); } exchange.CurrentResponse = block; @@ -254,6 +242,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo { if (block1 != null) response.SetOption(block1); + exchange.Request.Response = response; exchange.CurrentResponse = response; // Block1 transfer completed ClearBlockCleanup(exchange); @@ -265,9 +254,9 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Response response) { - log.Debug($"Transition of response through {this.GetType()}"); - log.Debug($"Response-Length: {response?.Bytes?.Length}"); - log.Debug($"Response-Options: {string.Join(" ", response?.GetOptions()?.Select(o => o.ToString())?.ToArray())}"); + log.LogTrace($"Transition of response through {this.GetType()}"); + log.LogTrace($"Response-Length: {response?.Bytes?.Length}"); + log.LogTrace($"Response-Options: {string.Join(" ", response?.GetOptions()?.Select(o => o.ToString())?.ToArray())}"); // do not continue fetching blocks if canceled if (exchange.Request.IsCancelled) @@ -275,8 +264,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re // reject (in particular for Block+Observe) if (response.Type != MessageType.ACK) { - if (log.IsDebugEnabled) - log.Debug("Rejecting blockwise transfer for canceled Exchange"); + log.LogTrace("Rejecting blockwise transfer for canceled Exchange"); EmptyMessage rst = EmptyMessage.NewRST(response); SendEmptyMessage(nextLayer, exchange, rst); // Matcher sets exchange as complete when RST is sent @@ -296,8 +284,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re if (block1 != null) { // TODO: What if request has not been sent blockwise (server error) - if (log.IsDebugEnabled) - log.Debug("Response acknowledges block " + block1); + log.LogTrace("Response acknowledges block " + block1); BlockwiseStatus status = exchange.RequestBlockStatus; if (!status.Complete) @@ -311,8 +298,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re // Send next block Int32 currentSize = 1 << (4 + status.CurrentSZX); Int32 nextNum = status.CurrentNUM + currentSize / block1.Size; - if (log.IsDebugEnabled) - log.Debug("Send next block num = " + nextNum); + log.LogTrace("Send next block num = " + nextNum); status.CurrentNUM = nextNum; status.CurrentSZX = block1.SZX; Request nextBlock = GetNextRequestBlock(exchange.Request, status); @@ -334,8 +320,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re } else { - if (log.IsDebugEnabled) - log.Debug("Response has Block2 option and is therefore sent blockwise"); + log.LogTrace("Response has Block2 option and is therefore sent blockwise"); } } @@ -363,8 +348,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re } else if (block2.M) { - if (log.IsDebugEnabled) - log.Debug("Request the next response block"); + log.LogTrace("Request the next response block"); Request request = exchange.Request; Int32 num = block2.NUM + 1; @@ -389,8 +373,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re } else { - if (log.IsDebugEnabled) - log.Debug("We have received all " + status.BlockCount + " blocks of the response. Assemble and deliver."); + log.LogTrace("We have received all " + status.BlockCount + " blocks of the response. Assemble and deliver."); Response assembled = new Response(response.StatusCode); AssembleMessage(status, assembled, response); assembled.Type = response.Type; @@ -408,8 +391,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re exchange.ResponseBlockStatus = null; } - if (log.IsDebugEnabled) - log.Debug("Assembled response: " + assembled); + log.LogTrace("Assembled response: " + assembled); exchange.Response = assembled; base.ReceiveResponse(nextLayer, exchange, assembled); } @@ -420,8 +402,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re // ERROR, wrong block number (server error) // TODO: This scenario is not specified in the draft. // Currently, we reject it and cancel the request. - if (log.IsWarnEnabled) - log.Warn("Wrong block number. Expected " + status.CurrentNUM + " but received " + block2.NUM + ". Reject response; exchange has failed."); + log.LogTrace("Wrong block number. Expected " + status.CurrentNUM + " but received " + block2.NUM + ". Reject response; exchange has failed."); if (response.Type == MessageType.CON) { EmptyMessage rst = EmptyMessage.NewRST(response); @@ -440,8 +421,7 @@ private void EarlyBlock2Negotiation(Exchange exchange, Request request) { BlockOption block2 = request.Block2; BlockwiseStatus status2 = new BlockwiseStatus(request.ContentType, block2.NUM, block2.SZX); - if (log.IsDebugEnabled) - log.Debug("Request with early block negotiation " + block2 + ". Create and set new Block2 status: " + status2); + log.LogTrace("Request with early block negotiation " + block2 + ". Create and set new Block2 status: " + status2); exchange.ResponseBlockStatus = status2; } } @@ -459,13 +439,11 @@ private BlockwiseStatus FindRequestBlockStatus(Exchange exchange, Request reques status = new BlockwiseStatus(request.ContentType); status.CurrentSZX = BlockOption.EncodeSZX(_defaultBlockSize); exchange.RequestBlockStatus = status; - if (log.IsDebugEnabled) - log.Debug("There is no assembler status yet. Create and set new Block1 status: " + status); + log.LogTrace("There is no assembler status yet. Create and set new Block1 status: " + status); } else { - if (log.IsDebugEnabled) - log.Debug("Current Block1 status: " + status); + log.LogTrace("Current Block1 status: " + status); } // sets a timeout to complete exchange PrepareBlockCleanup(exchange); @@ -485,13 +463,11 @@ private BlockwiseStatus FindResponseBlockStatus(Exchange exchange, Response resp status = new BlockwiseStatus(response.ContentType); status.CurrentSZX = BlockOption.EncodeSZX(_defaultBlockSize); exchange.ResponseBlockStatus = status; - if (log.IsDebugEnabled) - log.Debug("There is no blockwise status yet. Create and set new Block2 status: " + status); + log.LogTrace("There is no blockwise status yet. Create and set new Block2 status: " + status); } else { - if (log.IsDebugEnabled) - log.Debug("Current Block2 status: " + status); + log.LogTrace("Current Block2 status: " + status); } // sets a timeout to complete exchange PrepareBlockCleanup(exchange); @@ -520,6 +496,8 @@ private Request GetNextRequestBlock(Request request, BlockwiseStatus status) block.AddOption(new BlockOption(OptionType.Block1, num, szx, m)); block.MaxRetransmit = request.MaxRetransmit; block.TimedOut += (s, a) => request.IsTimedOut = true; + // inform main message of a retransmission + block.Retransmitting += (s, a) => request.FireRetransmitting(); status.Complete = !m; return block; } @@ -664,13 +642,11 @@ private void BlockwiseTimeout(Exchange exchange) { if (exchange.Request == null) { - if (log.IsInfoEnabled) - log.Info("Block1 transfer timed out: " + exchange.CurrentRequest); + log.LogTrace("Block1 transfer timed out: " + exchange.CurrentRequest); } else { - if (log.IsInfoEnabled) - log.Info("Block2 transfer timed out: " + exchange.Request); + log.LogTrace("Block2 transfer timed out: " + exchange.Request); } exchange.Complete = true; } diff --git a/WorldDirect.CoAP/Stack/ObserveLayer.cs b/WorldDirect.CoAP/Stack/ObserveLayer.cs index 0097ce7..a4ded22 100644 --- a/WorldDirect.CoAP/Stack/ObserveLayer.cs +++ b/WorldDirect.CoAP/Stack/ObserveLayer.cs @@ -14,12 +14,13 @@ namespace WorldDirect.CoAP.Stack using System; using System.Threading; using Log; + using Microsoft.Extensions.Logging; using Net; using Observe; public class ObserveLayer : AbstractLayer { - static readonly ILogger log = LogManager.GetLogger(typeof(ObserveLayer)); + static readonly ILogger log = LogManager.GetLogger(); static readonly Object ReregistrationContextKey = "ReregistrationContext"; /// @@ -46,8 +47,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo // Transmit errors as CON if (!Code.IsSuccess(response.Code)) { - if (log.IsDebugEnabled) - log.Debug("Response has error code " + response.Code + " and must be sent as CON"); + log.LogTrace("Response has error code " + response.Code + " and must be sent as CON"); response.Type = MessageType.CON; relation.Cancel(); } @@ -56,8 +56,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo // Make sure that every now and than a CON is mixed within if (relation.Check()) { - if (log.IsDebugEnabled) - log.Debug("The observe relation check requires the notification to be sent as CON"); + log.LogTrace("The observe relation check requires the notification to be sent as CON"); response.Type = MessageType.CON; } else @@ -103,8 +102,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo Response current = relation.CurrentControlNotification; if (current != null && IsInTransit(current)) { - if (log.IsDebugEnabled) - log.Debug("A former notification is still in transit. Postpone " + response); + log.LogTrace("A former notification is still in transit. Postpone " + response); // use the same ID response.ID = current.ID; relation.NextControlNotification = response; @@ -129,8 +127,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re if (exchange.Request.IsCancelled) { // The request was canceled and we no longer want notifications - if (log.IsDebugEnabled) - log.Debug("ObserveLayer rejecting notification for canceled Exchange"); + log.LogTrace("ObserveLayer rejecting notification for canceled Exchange"); EmptyMessage rst = EmptyMessage.NewRST(response); SendEmptyMessage(nextLayer, exchange, rst); // Matcher sets exchange as complete when RST is sent @@ -144,8 +141,7 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re // - "ReregistrationContext" takes into consideration the wrong request // This seems to be a bug - if (log.IsDebugEnabled) - log.Debug("Reregistration not supported"); + log.LogTrace("Reregistration not supported"); //PrepareReregistration(exchange, response, msg => SendRequest(nextLayer, exchange, msg)); @@ -197,8 +193,7 @@ private void PrepareSelfReplacement(INextLayer nextLayer, Exchange exchange, Res relation.NextControlNotification = null; if (next != null) { - if (log.IsDebugEnabled) - log.Debug("Notification has been acknowledged, send the next one"); + log.LogTrace("Notification has been acknowledged, send the next one"); // this is not a self replacement, hence a new ID next.ID = Message.None; // Create a new task for sending next response so that we can leave the sync-block @@ -215,8 +210,7 @@ private void PrepareSelfReplacement(INextLayer nextLayer, Exchange exchange, Res Response next = relation.NextControlNotification; if (next != null) { - if (log.IsDebugEnabled) - log.Debug("The notification has timed out and there is a fresher notification for the retransmission."); + log.LogTrace("The notification has timed out and there is a fresher notification for the retransmission."); // Cancel the original retransmission and send the fresh notification here response.IsCancelled = true; // use the same ID @@ -238,8 +232,7 @@ private void PrepareSelfReplacement(INextLayer nextLayer, Exchange exchange, Res response.TimedOut += (o, e) => { ObserveRelation relation = exchange.Relation; - if (log.IsDebugEnabled) - log.Debug("Notification" + relation.Exchange.Request.TokenString + log.LogTrace("Notification" + relation.Exchange.Request.TokenString + " timed out. Cancel all relations with source " + relation.Source); relation.CancelAll(); }; @@ -251,8 +244,7 @@ private void PrepareReregistration(Exchange exchange, Response response, Action< ReregistrationContext ctx = exchange.GetOrAdd( ReregistrationContextKey, _ => new ReregistrationContext(exchange, timeout, reregister)); - if (log.IsDebugEnabled) - log.Debug("Scheduling re-registration in " + timeout + "ms for " + exchange.Request); + log.LogTrace("Scheduling re-registration in " + timeout + "ms for " + exchange.Request); ctx.Restart(); } @@ -306,15 +298,13 @@ void timer_Elapsed(object target) refresh.Token = request.Token; refresh.Destination = request.Destination; refresh.CopyEventHandler(request); - if (log.IsDebugEnabled) - log.Debug("Re-registering for " + request); + log.LogTrace("Re-registering for " + request); request.FireReregister(refresh); _reregister(refresh); } else { - if (log.IsDebugEnabled) - log.Debug("Dropping re-registration for canceled " + request); + log.LogTrace("Dropping re-registration for canceled " + request); } } } diff --git a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs index 45f78b6..35bf04e 100644 --- a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs +++ b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs @@ -12,8 +12,10 @@ namespace WorldDirect.CoAP.Stack { using System; + using System.Diagnostics; using System.Threading; using Log; + using Microsoft.Extensions.Logging; using Net; /// @@ -21,7 +23,7 @@ namespace WorldDirect.CoAP.Stack /// public class ReliabilityLayer : AbstractLayer { - static readonly ILogger log = LogManager.GetLogger(typeof(ReliabilityLayer)); + static readonly ILogger log = LogManager.GetLogger(); static readonly Object TransmissionContextKey = "TransmissionContext"; private readonly Random _rand = new Random(); @@ -45,8 +47,6 @@ public override void SendRequest(INextLayer nextLayer, Exchange exchange, Reques if (request.Type == MessageType.CON) { - if (log.IsDebugEnabled) - log.Debug("Scheduling retransmission for " + request); PrepareRetransmission(exchange, request, ctx => SendRequest(nextLayer, exchange, request)); } @@ -93,8 +93,7 @@ public override void SendResponse(INextLayer nextLayer, Exchange exchange, Respo if (response.Type == MessageType.CON) { - if (log.IsDebugEnabled) - log.Debug("Scheduling retransmission for " + response); + log.LogTrace("Scheduling retransmission for " + response); PrepareRetransmission(exchange, response, ctx => SendResponse(nextLayer, exchange, response)); } @@ -117,30 +116,26 @@ public override void ReceiveRequest(INextLayer nextLayer, Exchange exchange, Req // Request is a duplicate, so resend ACK, RST or response if (exchange.CurrentResponse != null) { - if (log.IsDebugEnabled) - log.Debug("Respond with the current response to the duplicate request"); + log.LogTrace("Respond with the current response to the duplicate request"); base.SendResponse(nextLayer, exchange, exchange.CurrentResponse); } else if (exchange.CurrentRequest != null) { if (exchange.CurrentRequest.IsAcknowledged) { - if (log.IsDebugEnabled) - log.Debug("The duplicate request was acknowledged but no response computed yet. Retransmit ACK."); + log.LogTrace("The duplicate request was acknowledged but no response computed yet. Retransmit ACK."); EmptyMessage ack = EmptyMessage.NewACK(request); SendEmptyMessage(nextLayer, exchange, ack); } else if (exchange.CurrentRequest.IsRejected) { - if (log.IsDebugEnabled) - log.Debug("The duplicate request was rejected. Reject again."); + log.LogTrace("The duplicate request was rejected. Reject again."); EmptyMessage rst = EmptyMessage.NewRST(request); SendEmptyMessage(nextLayer, exchange, rst); } else { - if (log.IsDebugEnabled) - log.Debug("The server has not yet decided what to do with the request. We ignore the duplicate."); + log.LogTrace("The server has not yet decided what to do with the request. We ignore the duplicate."); // The server has not yet decided, whether to acknowledge or // reject the request. We know for sure that the server has // received the request though and can drop this duplicate here. @@ -175,16 +170,14 @@ public override void ReceiveResponse(INextLayer nextLayer, Exchange exchange, Re if (response.Type == MessageType.CON && !exchange.Request.IsCancelled) { - if (log.IsDebugEnabled) - log.Debug("Response is confirmable, send ACK."); + log.LogTrace("Response is confirmable, send ACK."); EmptyMessage ack = EmptyMessage.NewACK(response); SendEmptyMessage(nextLayer, exchange, ack); } if (response.Duplicate) { - if (log.IsDebugEnabled) - log.Debug("Response is duplicate, ignore it."); + log.LogTrace("Response is duplicate, ignore it."); } else { @@ -213,8 +206,7 @@ public override void ReceiveEmptyMessage(INextLayer nextLayer, Exchange exchange exchange.CurrentResponse.IsRejected = true; break; default: - if (log.IsWarnEnabled) - log.Warn("Empty messgae was not ACK nor RST: " + message); + log.LogTrace("Empty messgae was not ACK nor RST: " + message); break; } @@ -238,9 +230,7 @@ private void PrepareRetransmission(Exchange exchange, Message msg, Action _retransmit; + private ExecutionContext? _context; public TransmissionContext(ICoapConfig config, Exchange exchange, Message message, Action retransmit) { + _context = ExecutionContext.Capture()?.CreateCopy(); _config = config; _exchange = exchange; _message = message; @@ -307,15 +299,6 @@ public void Cancel() { // ignore } - - if (log.IsDebugEnabled) - { - log.Debug("Cancel retransmission for -->"); - if (_exchange.Origin == Origin.Local) - log.Debug(_exchange.CurrentRequest); - else - log.Debug(_exchange.CurrentResponse); - } } public void Dispose() @@ -324,6 +307,14 @@ public void Dispose() } void timer_Elapsed(object state) + { + if(this._context != null) + ExecutionContext.Run(this._context, this.OnExecutionContext, this); + else + this.OnExecutionContext(state); + } + + void OnExecutionContext(object _) { /* * Do not retransmit a message if it has been acknowledged, @@ -334,26 +325,22 @@ void timer_Elapsed(object state) if (_message.IsAcknowledged) { - if (log.IsDebugEnabled) - log.Debug("Timeout: message already acknowledged, cancel retransmission of " + _message); + log.LogTrace("Timeout: message already acknowledged, cancel retransmission of " + _message); return; } else if (_message.IsRejected) { - if (log.IsDebugEnabled) - log.Debug("Timeout: message already rejected, cancel retransmission of " + _message); + log.LogTrace("Timeout: message already rejected, cancel retransmission of " + _message); return; } else if (_message.IsCancelled) { - if (log.IsDebugEnabled) - log.Debug("Timeout: canceled (ID=" + _message.ID + "), do not retransmit"); + log.LogTrace("Timeout: canceled (ID=" + _message.ID + "), do not retransmit"); return; } else if (failedCount <= (_message.MaxRetransmit != 0 ? _message.MaxRetransmit : _config.MaxRetransmit)) { - if (log.IsDebugEnabled) - log.Debug("Timeout: retransmit message, failed: " + failedCount + ", message: " + _message); + log.LogDebug("Timeout: retransmit message for the {retry}. time.", failedCount); _message.FireRetransmitting(); @@ -363,8 +350,7 @@ void timer_Elapsed(object state) } else { - if (log.IsDebugEnabled) - log.Debug("Timeout: retransmission limit reached, exchange failed, message: " + _message); + log.LogDebug("Retransmission limit reached."); _exchange.TimedOut = true; _message.IsTimedOut = true; _exchange.Remove(TransmissionContextKey); diff --git a/WorldDirect.CoAP/Tracing.cs b/WorldDirect.CoAP/Tracing.cs new file mode 100644 index 0000000..16ba609 --- /dev/null +++ b/WorldDirect.CoAP/Tracing.cs @@ -0,0 +1,21 @@ +namespace WorldDirect.CoAP +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + + public static class Tracing + { + public static readonly string ActivityName = "WorldDirect.CoAP"; + /// + /// The activity source for the tracing client events. + /// + internal static readonly ActivitySource ClientSource = new ActivitySource(ActivityName, "1.0.0"); + + /// + /// The activity source for the tracing events. + /// + internal static readonly ActivitySource ServerSource = new ActivitySource(ActivityName, "1.0.0"); + } +} diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index fb4ed6a..e94a0fd 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -1,21 +1,24 @@ - + - netstandard2.0 - 0.5.7 + netstandard2.1 + 1.0.0-alpha.3 World-Direct eBusiness solutions GmbH 2021 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET - Changelog: - v0.5.7: Bugfix blockwise transfer - v0.5.6: Add progress reporting on long running Put - v0.5.5: Bugfix Stackoverflow - v0.5.4: Disabled logging - v0.5.3: ? - v0.5.2: Bugfix Logmanager - v0.5.1: Bugfix request timeout blockwise + + Changelog: + v0.6.1: add tracing diagnostics, fix message ids per endpoint + v0.6.0: adaption of logging + v0.5.7: Bugfix blockwise transfer + v0.5.6: Add progress reporting on long running Put + v0.5.5: Bugfix Stackoverflow + v0.5.4: Disabled logging + v0.5.3: ? + v0.5.2: Bugfix Logmanager + v0.5.1: Bugfix request timeout blockwise true @@ -41,7 +44,8 @@ - + + diff --git a/WorldDirect.CoAPS.DTLS.Specs/DTLS12KeyFileDataSpecs.cs b/WorldDirect.CoAPS.DTLS.Specs/DTLS12KeyFileDataSpecs.cs new file mode 100644 index 0000000..1830bd7 --- /dev/null +++ b/WorldDirect.CoAPS.DTLS.Specs/DTLS12KeyFileDataSpecs.cs @@ -0,0 +1,22 @@ +namespace WorldDirect.CoAPS.DTLS.Specs +{ + using CoAP.DTLS; + using FluentAssertions; + using Org.BouncyCastle.Tls.Crypto.Impl.BC; + + public class DTLS12KeyFileDataSpecs + { + [Fact] + public void CanExtractSecret() + { + var secretData = new byte[] { 0x01, 0x02 }; + var secret = new BcTlsSecret(new BcTlsCrypto(), secretData); + var clientRandom = new byte[] { 0x03, 0x04 }; + + var keyData = DTLS12KeyFileData.FromSecret(clientRandom, secret); + + keyData.Should().NotBeNull(); + keyData.Value.PreMasterSecret.Should().BeEquivalentTo(secretData); + } + } +} diff --git a/WorldDirect.CoAPS.DTLS.Specs/Usings.cs b/WorldDirect.CoAPS.DTLS.Specs/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/WorldDirect.CoAPS.DTLS.Specs/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/WorldDirect.CoAPS.DTLS.Specs/WorldDirect.CoAPS.DTLS.Specs.csproj b/WorldDirect.CoAPS.DTLS.Specs/WorldDirect.CoAPS.DTLS.Specs.csproj new file mode 100644 index 0000000..4453e2f --- /dev/null +++ b/WorldDirect.CoAPS.DTLS.Specs/WorldDirect.CoAPS.DTLS.Specs.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +