From 4cae3e135bcd505f125f986855892cd19b731bab Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Thu, 13 Apr 2023 14:06:40 +0200 Subject: [PATCH 01/27] better abstraction --- EnergySoultions.CoAP.sln | 18 +- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 482 ++++++++++++++++++ WorldDirect.CoAP.DTLS/DTLSChannel.cs | 60 +++ WorldDirect.CoAP.DTLS/DTLSServer.cs | 109 ++++ WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs | 110 ++++ WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 10 + WorldDirect.CoAP.DTLS/DTLSSession.cs | 112 ++++ WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs | 8 + WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 96 ++++ WorldDirect.CoAP.DTLS/EcServerCertificate.cs | 17 + WorldDirect.CoAP.DTLS/IDTLSFactory.cs | 6 + WorldDirect.CoAP.DTLS/IUDPSender.cs | 8 + WorldDirect.CoAP.DTLS/UdpChannelSender.cs | 18 + WorldDirect.CoAP.DTLS/UdpTransport.cs | 77 +++ .../WorldDirect.CoAP.DTLS.csproj | 18 + WorldDirect.CoAP/Net/Matcher.cs | 2 +- 16 files changed, 1148 insertions(+), 3 deletions(-) create mode 100644 WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSChannel.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSServer.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSServerConfig.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSSession.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSSessionManager.cs create mode 100644 WorldDirect.CoAP.DTLS/EcServerCertificate.cs create mode 100644 WorldDirect.CoAP.DTLS/IDTLSFactory.cs create mode 100644 WorldDirect.CoAP.DTLS/IUDPSender.cs create mode 100644 WorldDirect.CoAP.DTLS/UdpChannelSender.cs create mode 100644 WorldDirect.CoAP.DTLS/UdpTransport.cs create mode 100644 WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj diff --git a/EnergySoultions.CoAP.sln b/EnergySoultions.CoAP.sln index cdddf2f..d76dbf2 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,8 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.DTLS", "WorldDirect.CoAP.DTLS\WorldDirect.CoAP.DTLS.csproj", "{22366801-AB3F-4F99-AD26-80B6755F0709}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,18 @@ 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 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..36c8e73 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -0,0 +1,482 @@ +/* + * 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.Serialization; + using System.Threading; + using Channel; + using Codec; + using DTLS; + using Log; + using Microsoft.Extensions.Caching.Memory; + using Org.BouncyCastle.Tls; + using Stack; + using Threading; + + /// + /// EndPoint encapsulates the dtlsStack that executes the CoAP protocol. + /// + public partial class CoAPSEndpoint : IEndPoint, IOutbox + { + static readonly ILogger log = LogManager.GetLogger(typeof(CoAPSEndpoint)); + + readonly ICoapConfig _config; + readonly CoapStack _coapStack; + private IMessageDeliverer _deliverer; + private IMatcher _matcher; + private Int32 _running; + private System.Net.EndPoint _localEP; + private IExecutor _executor; + private DTLSChannel channel; + + /// + 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, IDTLSFactory factory) + { + _config = CoapConfig.Default; + _matcher = new Matcher(this._config); + _coapStack = new CoapStack(this._config); + UDPChannel channel = new UDPChannel(new IPEndPoint(IPAddress.Any, 5684)); + channel.ReceiveBufferSize = this._config.ChannelReceiveBufferSize; + channel.SendBufferSize = this._config.ChannelSendBufferSize; + channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; + this.channel = new DTLSChannel(channel, cache, factory); + this.channel.DataReceived += Channel_DataReceived; + } + + + /// + 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 + { + get { return _localEP; } + } + + /// + 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 + { + if (log.IsWarnEnabled) + log.Warn("Cannot start endpoint at " + this.channel.LocalEndPoint); + Stop(); + throw; + } + if (log.IsDebugEnabled) + log.Debug("Starting endpoint bound to " + this.channel.LocalEndPoint); + } + + /// + public void Stop() + { + if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) + return; + if (log.IsDebugEnabled) + log.Debug("Stopping endpoint bound to " + _localEP); + 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 Channel_DataReceived(object? sender, DataReceivedEventArgs e) + { + IMessageDecoder decoder = Spec.NewMessageDecoder(e.Data); + if (decoder.IsRequest) + { + Request request; + try + { + request = decoder.DecodeRequest(); + } + catch (Exception) + { + if (decoder.IsReply) + { + if (log.IsWarnEnabled) + log.Warn("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); + + if (log.IsWarnEnabled) + log.Warn("Message format error caused by " + e.EndPoint + " and reseted."); + } + return; + } + + request.Source = e.EndPoint; + + Fire(ReceivingRequest, request); + + if (!request.IsCancelled) + { + Exchange exchange = _matcher.ReceiveRequest(request); + if (exchange != null) + { + exchange.EndPoint = this; + //exchange.Set(nameof(DTLSClient), e.Remote); + _coapStack.ReceiveRequest(exchange, request); + } + } + } + else if (decoder.IsResponse) + { + Response response = decoder.DecodeResponse(); + response.Source = e.EndPoint; + + Fire(ReceivingResponse, response); + + if (!response.IsCancelled) + { + Exchange exchange = _matcher.ReceiveResponse(response); + if (exchange != null) + { + response.RTT = (DateTime.Now - exchange.Timestamp).TotalMilliseconds; + exchange.EndPoint = this; + _coapStack.ReceiveResponse(exchange, response); + } + else if (response.Type != MessageType.ACK) + { + if (log.IsDebugEnabled) + log.Debug("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) + { + if (log.IsDebugEnabled) + log.Debug("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 if (log.IsDebugEnabled) + { + log.Debug("Silently ignoring non-CoAP message from " + e.EndPoint); + } + } + + /*private void ReceiveData(DTLSDecryptedDataReceivedEventArgs e) + { + IMessageDecoder decoder = Spec.NewMessageDecoder(e.Payload); + if (decoder.IsRequest) + { + Request request; + try + { + request = decoder.DecodeRequest(); + } + catch (Exception) + { + if (decoder.IsReply) + { + if (log.IsWarnEnabled) + log.Warn("Message format error caused by " + e.Remote.Remote); + } + else + { + // manually build RST from raw information + EmptyMessage rst = new EmptyMessage(MessageType.RST); + rst.Destination = e.Remote.Remote; + rst.ID = decoder.ID; + + Fire(SendingEmptyMessage, rst); + + _dtlsStack.SendTo(Serialize(rst), e.Remote.Remote); + + if (log.IsWarnEnabled) + log.Warn("Message format error caused by " + e.Remote.Remote + " and reseted."); + } + return; + } + + request.Source = e.Remote.Remote; + + Fire(ReceivingRequest, request); + + if (!request.IsCancelled) + { + Exchange exchange = _matcher.ReceiveRequest(request); + if (exchange != null) + { + exchange.EndPoint = this; + exchange.Set(nameof(DTLSClient), e.Remote); + _coapStack.ReceiveRequest(exchange, request); + } + } + } + else if (decoder.IsResponse) + { + Response response = decoder.DecodeResponse(); + response.Source = e.Remote.Remote; + + Fire(ReceivingResponse, response); + + if (!response.IsCancelled) + { + Exchange exchange = _matcher.ReceiveResponse(response); + if (exchange != null) + { + response.RTT = (DateTime.Now - exchange.Timestamp).TotalMilliseconds; + exchange.EndPoint = this; + _coapStack.ReceiveResponse(exchange, response); + } + else if (response.Type != MessageType.ACK) + { + if (log.IsDebugEnabled) + log.Debug("Rejecting unmatchable response from " + e.Remote.Remote); + Reject(response); + } + } + } + else if (decoder.IsEmpty) + { + EmptyMessage message = decoder.DecodeEmptyMessage(); + message.Source = e.Remote.Remote; + + Fire(ReceivingEmptyMessage, message); + + if (!message.IsCancelled) + { + // CoAP Ping + if (message.Type == MessageType.CON || message.Type == MessageType.NON) + { + if (log.IsDebugEnabled) + log.Debug("Responding to ping by " + e.Remote.Remote); + Reject(message); + } + else + { + Exchange exchange = _matcher.ReceiveEmptyMessage(message); + if (exchange != null) + { + exchange.EndPoint = this; + _coapStack.ReceiveEmptyMessage(exchange, message); + } + } + } + } + else if (log.IsDebugEnabled) + { + log.Debug("Silently ignoring non-CoAP message from " + e.Remote.Remote); + } + }*/ + + 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/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs new file mode 100644 index 0000000..cf018bc --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -0,0 +1,60 @@ +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; + + public class DTLSChannel : IChannel + { + private readonly UDPChannel channel; + private readonly DTLSSessionManager sessionManager; + + public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory) + { + this.channel = channel; + this.channel.DataReceived += DtlsReceived; + // todo configure sessiontimeout + var config = new DTLSSessionConfig() {MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = TimeSpan.FromMinutes(3),}; + this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), factory, config); + } + + private void DtlsReceived(object? sender, DataReceivedEventArgs e) + { + this.sessionManager.ReceivedUdpPacket(e.Data, e.EndPoint); + } + + public void Dispose() + { + this.Stop(); + } + + public EndPoint LocalEndPoint => this.channel.LocalEndPoint; + public event EventHandler? DataReceived; + 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) + { + this.sessionManager.SendTo(data, ep); + } + + private void DecryptedForwarding(object? sender, DataReceivedEventArgs e) + { + this.DataReceived?.Invoke(this, e); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs new file mode 100644 index 0000000..63c81b8 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -0,0 +1,109 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Pkix; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; + +public class DTLSServer : AbstractTlsServer +{ + private readonly DTLSServerConfig config; + + public DTLSServer(BcTlsCrypto crypto, DTLSServerConfig config) : base(crypto) + { + this.config = config; + } + + public string PeerPublicIdentifier { get; private set; } + + // todo X509Certificate dotnet + public X509Certificate? PeerCertificate { get; private set; } + + protected override ProtocolVersion[] GetSupportedVersions() + { + return ProtocolVersion.DTLSv12.Only(); + } + + protected override int[] GetSupportedCipherSuites() + { + return this.config.CipherSuites.ToArray(); + } + + public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() + { + var serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); + // currently only ecdsa supported + serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); + + // send back a list of supported CAs (if wanted) + var authorities = this.config.CAs.Select(ca => ca.SubjectDN).ToList(); + short[] certificateTypes = new short[] { ClientCertificateType.ecdsa_sign, }; + + return new CertificateRequest(certificateTypes, serverSigAlgs, authorities); + } + + public override TlsCredentials GetCredentials() + { + int keyExchangeAlgorithm = m_context.SecurityParameters.KeyExchangeAlgorithm; + switch (keyExchangeAlgorithm) + { + case KeyExchangeAlgorithm.ECDHE_ECDSA: + return GetECDsaSignerCredentials(); + default: + throw new TlsFatalAlert(AlertDescription.handshake_failure, "Unsupported exchange algorithm"); + } + } + + 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(ca => new TrustAnchor(ca, null)); + + var parameters = new PkixParameters(new SortedSet(trustAnchors)); + parameters.IsRevocationEnabled = false; + var path = new PkixCertPath(chainAsCertificate); + var validator = new PkixCertPathValidator(); + validator.Validate(path, parameters); + } + + private TlsCredentialedSigner GetECDsaSignerCredentials() + { + var serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); + // currently only ecdsa supported + serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); + 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) + { + // 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.EcCertificates.First(); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs new file mode 100644 index 0000000..2e9496d --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs @@ -0,0 +1,110 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using Org.BouncyCastle.X509; + +/// +/// +/// +/// +/// Limitations: +/// only one ECDSA Chain Certificate work (not RSA, not multiple) +/// +public class DTLSServerBuilder +{ + private readonly BcTlsCrypto crypto; + private Pkcs12Store? store; + private List CAs = new(); + + private readonly DTLSServerConfig config; + + public DTLSServerBuilder() + { + this.crypto = new BcTlsCrypto(new SecureRandom()); + this.config = new DTLSServerConfig(); + } + + public DTLSServerBuilder WithStore(string file, string password) + { + var store = new Pkcs12StoreBuilder().Build(); + using var reader = File.OpenRead(file); + store.Load(reader, password.ToCharArray()); + this.store = store; + return this; + } + + /// + /// Loads the certificate chain and its private key from the store. The store must be loaded before with function. + /// + /// The alias which identifies the certificate. + /// + public DTLSServerBuilder WithEcdsaCertificate(string alias) + { + // check if certificate is ecdsa certificate. + var certificateChain = this.store!.GetCertificateChain(alias); + var key = this.store!.GetKey(alias); + + var serverCert = certificateChain[0].Certificate; + var der = new DerObjectIdentifier(serverCert.SigAlgOid); + if (!der.On(X9ObjectIdentifiers.id_ecSigType)) + { + // signature algorithm of certificate is not ECDSA + throw new InvalidOperationException( + $"Provided certificate of {alias} was signed with {serverCert.SigAlgName}. This not an ECDSA algorithm"); + } + if (key.Key.GetType() != typeof(ECPrivateKeyParameters)) + { + throw new InvalidOperationException($"Provided key of {alias} is not an ECKey"); + } + + var x509Certs = certificateChain.Select(c => this.crypto.CreateCertificate(c.Certificate.GetEncoded())); + + var ecPrivateKey = (key.Key as ECPrivateKeyParameters)!; + var ecdsaCertificate = new Certificate(x509Certs.ToArray()); + + this.config.EcCertificates.Add(new EcServerCertificate(ecdsaCertificate, ecPrivateKey)); + if (this.config.EcCertificates.Count > 1) + { + throw new InvalidOperationException("Currently one one certificate is supported."); + } + return this; + } + + /// + /// Loads a trusted root CA from a pem encoded file. + /// + /// The filename fo load the CA from. + /// + /// + /// + public DTLSServerBuilder WithTrustedRoot(string filename) + { + X509CertificateParser parser = new X509CertificateParser(); + using var file = File.Open(filename, FileMode.Open); + var cert = parser.ReadCertificate(file); + if (cert == null) + { + throw new InvalidOperationException($"Could not read certificate from {filename}"); + } + this.config.CAs.Add(cert); + return this; + } + + public DTLSServerBuilder WithCipherSuites(IEnumerable suites) + { + this.config.CipherSuites.AddRange(suites); + // todo check if possibility to check if selected suites are valid with current configuration (ECDSA & RSA avaiable? configured psk?) + return this; + } + + public DTLSServer Build() + { + return new DTLSServer(this.crypto, this.config); + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs new file mode 100644 index 0000000..b9cbe7b --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -0,0 +1,10 @@ +namespace WorldDirect.CoAP.DTLS; + +public class DTLSServerConfig +{ + public List EcCertificates { get; set; } = new(); + + public List CAs { get; set; } = new (); + + public List CipherSuites { get; set; } = new(); +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs new file mode 100644 index 0000000..11e07d9 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -0,0 +1,112 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; +using Channel; +using Org.BouncyCastle.Tls; + +internal class DTLSSession +{ + private readonly CancellationTokenSource cts; + private readonly DTLSSessionConfig config; + private Task? HandleTask; + private readonly UdpTransport transport; + private readonly DtlsServerProtocol protocol; + private readonly DTLSServer dtlsServer; + private DtlsTransport? dtlsTransport; + + public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, CancellationTokenSource cts, DTLSSessionConfig config) + { + this.cts = cts; + this.config = config; + this.transport = new UdpTransport(sender, remote, config.MaxPacketLength); + this.protocol = new DtlsServerProtocol(); + this.dtlsServer = server; + } + + /// + /// An event when a new decrypted payload was received. + /// + public event EventHandler? DataReceived; + + /// + /// Cancel the task which handles the the received data. + /// + public void Cancel() + { + this.cts.Cancel(); + } + + /// + /// Start the task which handles the received data. + /// + public void Start() + { + this.HandleTask = Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); + } + + /// + /// Send the specified plaintext payload encrypted with this session. + /// + /// The plaintext payload. + public void Send(ReadOnlySpan payload) + { + this.dtlsTransport?.Send(payload); + } + + /// + /// Enqueue a received dtls message for this session. + /// + /// The dtls message. + public void Enqueue(ReadOnlySpan payload) + { + this.transport.Enqueue(payload); + } + + private async Task HandleSession() + { + try + { + + await this.HandleSessionAsync(this.cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + + } + catch (Exception e) + { + // Todo logging + } + finally + { + // todo logging + this.HandleTask = null; + } + } + + private async Task HandleSessionAsync(CancellationToken ct) + { + // accept + + this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); + var rxBuffer = new byte[this.config.MaxPacketLength]; + + do + { + + var receivedMessage = await this.transport.WaitForMessageAsync(this.config.SessionTimeout, ct).ConfigureAwait(false); + if (!receivedMessage) + { + return; + } + + var length = this.dtlsTransport.Receive(rxBuffer, 0); + if (length <= 0) + { + throw new InvalidOperationException("Could not read from dtls"); + } + this.DataReceived?.Invoke(this, new DataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote)); + + } while (!ct.IsCancellationRequested); + } +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs new file mode 100644 index 0000000..9ea5760 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs @@ -0,0 +1,8 @@ +namespace WorldDirect.CoAP.DTLS; + +public class DTLSSessionConfig +{ + public TimeSpan SessionTimeout { get; set; } + + public int MaxPacketLength { get; set; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs new file mode 100644 index 0000000..46ea529 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -0,0 +1,96 @@ +namespace WorldDirect.CoAP.DTLS +{ + using System; + using System.Net; + using System.Text; + using Channel; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Internal; + using Org.BouncyCastle.Asn1.Nist; + using Org.BouncyCastle.Asn1.X509; + + /// + /// Handles all dtls related traffic. + /// + public class DTLSSessionManager + { + private readonly IMemoryCache cache; + private readonly IUDPSender sender; + private readonly IDTLSFactory factory; + private readonly DTLSSessionConfig config; + private readonly CancellationTokenSource cts; + + /// + /// Initializes a new instance of the class. + /// + /// A cache to store the sessions. + /// The configuration for the sessions. + public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, IDTLSFactory factory, DTLSSessionConfig config) + { + this.cache = cache; + this.sender = sender; + this.factory = factory; + this.config = config; + this.cts = new CancellationTokenSource(); + } + + /// + /// 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) + { + if(this.cache.TryGetValue(endPoint, out var session)) + { + session.Send(packet); + } + } + + public void Stop() + { + this.cts.Cancel(); + } + + /// + /// 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 session = this.cache.GetOrCreate(endPoint, entry => + { + entry.AbsoluteExpiration = DateTimeOffset.Now + config.SessionTimeout; + var callback = new PostEvictionCallbackRegistration() + { + EvictionCallback = OnEviction, + }; + entry.PostEvictionCallbacks.Add(callback); + + var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token), this.config); + s.DataReceived += DecryptedReceived; + s.Start(); + return s; + }); + + session.Enqueue(packet); + } + + private void DecryptedReceived(object? _, DataReceivedEventArgs e) + { + this.DataReceived?.Invoke(this, e); + } + + private static void OnEviction(object key, object value, EvictionReason reason, object state) + { + var obj = value as DTLSSession; + obj?.Cancel(); + } + } +} diff --git a/WorldDirect.CoAP.DTLS/EcServerCertificate.cs b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs new file mode 100644 index 0000000..2b67fd8 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs @@ -0,0 +1,17 @@ +namespace WorldDirect.CoAP.DTLS; + +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Tls; + +public class EcServerCertificate +{ + public EcServerCertificate(Certificate certificate, ECPrivateKeyParameters privateKey) + { + this.Certificate = certificate; + this.PrivateKey = privateKey; + } + + public Certificate Certificate { get; } + + public ECPrivateKeyParameters PrivateKey { get; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/IDTLSFactory.cs b/WorldDirect.CoAP.DTLS/IDTLSFactory.cs new file mode 100644 index 0000000..e70b23b --- /dev/null +++ b/WorldDirect.CoAP.DTLS/IDTLSFactory.cs @@ -0,0 +1,6 @@ +namespace WorldDirect.CoAP.DTLS; + +public interface IDTLSFactory +{ + public DTLSServer CreateServer(); +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/IUDPSender.cs b/WorldDirect.CoAP.DTLS/IUDPSender.cs new file mode 100644 index 0000000..3a9d010 --- /dev/null +++ b/WorldDirect.CoAP.DTLS/IUDPSender.cs @@ -0,0 +1,8 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; + +public interface IUDPSender +{ + void SendTo(ReadOnlySpan payload, EndPoint remote); +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs new file mode 100644 index 0000000..132e3ee --- /dev/null +++ b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs @@ -0,0 +1,18 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Net; +using Channel; + +public class UdpChannelSender : IUDPSender +{ + private readonly UDPChannel channel; + + public UdpChannelSender(UDPChannel channel) + { + this.channel = channel; + } + public void SendTo(ReadOnlySpan payload, EndPoint remote) + { + this.channel.Send(payload.ToArray(), remote); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs new file mode 100644 index 0000000..a79792d --- /dev/null +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -0,0 +1,77 @@ +namespace WorldDirect.CoAP.DTLS; + +using System.Collections.Concurrent; +using System.Net; +using Org.BouncyCastle.Tls; + +internal class UdpTransport : DatagramTransport +{ + private readonly IUDPSender sender; + private readonly int maxPacketLength; + private readonly ConcurrentQueue messages = new (); + private readonly SemaphoreSlim sema = new (0); + + public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength) + { + this.Remote = remote; + this.sender = sender; + this.maxPacketLength = maxPacketLength; + } + + public Task WaitForMessageAsync(TimeSpan timeout, CancellationToken ct) + { + return this.sema.WaitAsync(timeout, ct); + } + + public EndPoint Remote { get; } + + public int GetReceiveLimit() + { + return this.maxPacketLength; + } + + public int Receive(byte[] buf, int off, int len, int waitMillis) + { + return this.Receive(buf.AsSpan(off, len), waitMillis); + } + + public int Receive(Span buffer, int waitMillis) + { + if (this.sema.Wait(TimeSpan.FromMilliseconds(waitMillis))) + { + if (this.messages.TryDequeue(out var rx)) + { + rx.CopyTo(buffer); + return rx.Length > buffer.Length ? buffer.Length : rx.Length; + } + } + + return 0; + } + + public int GetSendLimit() + { + return this.maxPacketLength; + } + + public void Send(byte[] buf, int off, int len) + { + this.Send(buf.AsSpan(off, len)); + } + + public void Send(ReadOnlySpan buffer) + { + this.sender.SendTo(buffer, this.Remote); + } + + public void Close() + { + + } + + internal void Enqueue(ReadOnlySpan payload) + { + this.messages.Enqueue(payload.ToArray()); + this.sema.Release(); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj new file mode 100644 index 0000000..298f70a --- /dev/null +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/WorldDirect.CoAP/Net/Matcher.cs b/WorldDirect.CoAP/Net/Matcher.cs index 1fd22b2..ddfdd6f 100644 --- a/WorldDirect.CoAP/Net/Matcher.cs +++ b/WorldDirect.CoAP/Net/Matcher.cs @@ -19,7 +19,7 @@ namespace WorldDirect.CoAP.Net using Observe; using Util; - class Matcher : IMatcher, IDisposable + public class Matcher : IMatcher, IDisposable { static readonly ILogger log = LogManager.GetLogger(typeof(Matcher)); From 47ddf5c873b49cc00c94ded4e3699663f1cb1824 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 19 Apr 2023 14:48:07 +0200 Subject: [PATCH 02/27] add configuration of coap server in servicecollection --- EnergySoultions.CoAP.sln | 28 + WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 21 +- WorldDirect.CoAP.DTLS/DTLSServer.cs | 25 +- WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs | 6 + WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 10 +- WorldDirect.CoAP.DTLS/DTLSSession.cs | 75 +-- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 13 +- WorldDirect.CoAP.DTLS/UdpTransport.cs | 7 +- .../WorldDirect.CoAP.DTLS.csproj | 3 +- .../Configuration/BindingAddressSpecs.cs | 65 +++ .../Configuration/CertificationConfigSpecs.cs | 174 ++++++ .../Configuration/ConfigurationReaderSpecs.cs | 86 +++ ...Direct.CoAP.Server.Extensions.Specs.csproj | 35 ++ .../Configuration/CertificateConfig.cs | 33 ++ .../Configuration/CoAPServerOptions.cs | 152 +++++ .../Configuration/EndpointConfig.cs | 124 +++++ WorldDirect.CoAP.Server.Extensions/README.md | 66 +++ .../ServiceProviderExtensions.cs | 518 ++++++++++++++++++ .../WorldDirect.CoAP.Server.Extensions.csproj | 20 + 19 files changed, 1401 insertions(+), 60 deletions(-) create mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs create mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs create mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs create mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/README.md create mode 100644 WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj diff --git a/EnergySoultions.CoAP.sln b/EnergySoultions.CoAP.sln index d76dbf2..0500eb4 100644 --- a/EnergySoultions.CoAP.sln +++ b/EnergySoultions.CoAP.sln @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Example.Se EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.DTLS", "WorldDirect.CoAP.DTLS\WorldDirect.CoAP.DTLS.csproj", "{22366801-AB3F-4F99-AD26-80B6755F0709}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.Server.Extensions", "WorldDirect.CoAP.Server.Extensions\WorldDirect.CoAP.Server.Extensions.csproj", "{7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.Server.Extensions.Specs", "WorldDirect.CoAP.Server.Extensions.Specs\WorldDirect.CoAP.Server.Extensions.Specs.csproj", "{C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -93,6 +97,30 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 36c8e73..53a84d1 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -36,7 +36,6 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox private IMessageDeliverer _deliverer; private IMatcher _matcher; private Int32 _running; - private System.Net.EndPoint _localEP; private IExecutor _executor; private DTLSChannel channel; @@ -70,6 +69,15 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory) this.channel.DataReceived += Channel_DataReceived; } + public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel) + { + _config = CoapConfig.Default; + _matcher = new Matcher(this._config); + _coapStack = new CoapStack(this._config); + this.channel = new DTLSChannel(channel, cache, factory); + this.channel.DataReceived += Channel_DataReceived; + } + /// public ICoapConfig Config @@ -88,10 +96,7 @@ public IExecutor Executor } /// - public System.Net.EndPoint LocalEndPoint - { - get { return _localEP; } - } + public System.Net.EndPoint LocalEndPoint => this.channel.LocalEndPoint; /// public IMessageDeliverer MessageDeliverer @@ -134,12 +139,12 @@ public void Start() catch { if (log.IsWarnEnabled) - log.Warn("Cannot start endpoint at " + this.channel.LocalEndPoint); + log.Warn("Cannot start secure endpoint at " + this.channel.LocalEndPoint); Stop(); throw; } if (log.IsDebugEnabled) - log.Debug("Starting endpoint bound to " + this.channel.LocalEndPoint); + log.Debug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); } /// @@ -148,7 +153,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.Debug("Stopping secure endpoint bound to " + this.LocalEndPoint); this.channel.Stop(); _matcher.Stop(); _matcher.Clear(); diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index 63c81b8..23f3599 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -15,10 +15,12 @@ public DTLSServer(BcTlsCrypto crypto, DTLSServerConfig config) : base(crypto) this.config = config; } - public string PeerPublicIdentifier { get; private set; } + public TlsCertificate? PeerCertificate => this.m_context.SecurityParameters.PeerCertificate.IsEmpty ? null : this.m_context.SecurityParameters.PeerCertificate.GetCertificateAt(0); - // todo X509Certificate dotnet - public X509Certificate? PeerCertificate { get; private set; } + public override int GetHandshakeTimeoutMillis() + { + return (int)this.config.HandshakeTimeout.TotalMilliseconds; + } protected override ProtocolVersion[] GetSupportedVersions() { @@ -32,12 +34,20 @@ protected override int[] GetSupportedCipherSuites() public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() { + // if no CAs are registered, we wont need a certificate for authentication. + if (this.config.CAs.Count == 0) + { + 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 (if wanted) + // send back a list of supported CAs var authorities = this.config.CAs.Select(ca => ca.SubjectDN).ToList(); + short[] certificateTypes = new short[] { ClientCertificateType.ecdsa_sign, }; return new CertificateRequest(certificateTypes, serverSigAlgs, authorities); @@ -75,9 +85,6 @@ public override void NotifyClientCertificate(Certificate clientCertificate) private TlsCredentialedSigner GetECDsaSignerCredentials() { - var serverSigAlgs = TlsUtilities.GetDefaultSupportedSignatureAlgorithms(m_context); - // currently only ecdsa supported - serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); var clientSupportedSigAlgs = this.m_context.SecurityParameters.ClientSigAlgs; var clientECDsaSigAlgs = clientSupportedSigAlgs.Where(sig => sig.Signature == SignatureAlgorithm.ecdsa); if (!clientECDsaSigAlgs.Any()) @@ -100,10 +107,10 @@ private TlsCredentialedSigner GetECDsaSignerCredentials() private EcServerCertificate FindCertificate(IList names) { - // implement if multiple certificates are required. + // 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.EcCertificates.First(); } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs index 2e9496d..53129ee 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs @@ -39,6 +39,12 @@ public DTLSServerBuilder WithStore(string file, string password) return this; } + public DTLSServerBuilder WithHandShakeTimeout(TimeSpan timeout) + { + this.config.HandshakeTimeout = timeout; + return this; + } + /// /// Loads the certificate chain and its private key from the store. The store must be loaded before with function. /// diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs index b9cbe7b..a2f138a 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -7,4 +7,12 @@ public class DTLSServerConfig public List CAs { get; set; } = new (); public List CipherSuites { get; set; } = new(); -} \ No newline at end of file + + /// + /// Gets or sets the timeout of the dtls handshake. + /// + /// + /// 0 means no timeout + /// + public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 11e07d9..4f080f0 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -4,15 +4,21 @@ using Channel; using Org.BouncyCastle.Tls; +internal class HandshakeFinishedEventArgs : EventArgs +{ + public bool Successful { get; set; } +} + internal class DTLSSession { private readonly CancellationTokenSource cts; private readonly DTLSSessionConfig config; - private Task? HandleTask; private readonly UdpTransport transport; private readonly DtlsServerProtocol protocol; private readonly DTLSServer dtlsServer; private DtlsTransport? dtlsTransport; + private Task? HandshakeTask; + private bool HandshakeFailed = false; public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, CancellationTokenSource cts, DTLSSessionConfig config) { @@ -23,11 +29,15 @@ public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, Cancel this.dtlsServer = server; } + public EndPoint Remote => this.transport.Remote; + /// /// An event when a new decrypted payload was received. /// public event EventHandler? DataReceived; + public event EventHandler? HandshakeFinished; + /// /// Cancel the task which handles the the received data. /// @@ -41,7 +51,8 @@ public void Cancel() /// public void Start() { - this.HandleTask = Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); + // perform handshake asynchronously, would be blocking otherwise + Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); } /// @@ -59,54 +70,50 @@ public void Send(ReadOnlySpan payload) /// 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) + { + var rxBuffer = new byte[this.config.MaxPacketLength]; + var length = this.dtlsTransport!.Receive(rxBuffer, 0); + if (length < 0) + { + throw new InvalidOperationException("Could not read from dtls"); + } + else if (length > 0) + { + this.DataReceived?.Invoke(this, new DataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote)); + } + + } } private async Task HandleSession() { try { - - await this.HandleSessionAsync(this.cts.Token).ConfigureAwait(false); + // perform handshake + this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); + // todo logging } - catch (OperationCanceledException) + catch (TlsTimeoutException e) { - + // todo logging handshake timed out + this.HandshakeFailed = true; } catch (Exception e) { // Todo logging + this.HandshakeFailed = true; } finally { - // todo logging - this.HandleTask = null; + this.HandshakeFinished?.Invoke(this, new HandshakeFinishedEventArgs() { Successful = !this.HandshakeFailed }); } } - - private async Task HandleSessionAsync(CancellationToken ct) - { - // accept - - this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); - var rxBuffer = new byte[this.config.MaxPacketLength]; - - do - { - - var receivedMessage = await this.transport.WaitForMessageAsync(this.config.SessionTimeout, ct).ConfigureAwait(false); - if (!receivedMessage) - { - return; - } - - var length = this.dtlsTransport.Receive(rxBuffer, 0); - if (length <= 0) - { - throw new InvalidOperationException("Could not read from dtls"); - } - this.DataReceived?.Invoke(this, new DataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote)); - - } while (!ct.IsCancellationRequested); - } } diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 46ea529..85fc2a0 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -14,6 +14,7 @@ /// public class DTLSSessionManager { + // todo X509Certificate dotnet forwarding private readonly IMemoryCache cache; private readonly IUDPSender sender; private readonly IDTLSFactory factory; @@ -66,7 +67,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { var session = this.cache.GetOrCreate(endPoint, entry => { - entry.AbsoluteExpiration = DateTimeOffset.Now + config.SessionTimeout; + entry.SlidingExpiration = config.SessionTimeout; var callback = new PostEvictionCallbackRegistration() { EvictionCallback = OnEviction, @@ -75,6 +76,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token), this.config); s.DataReceived += DecryptedReceived; + s.HandshakeFinished += HandshakeFinished; s.Start(); return s; }); @@ -82,6 +84,15 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) session.Enqueue(packet); } + private void HandshakeFinished(object? sender, HandshakeFinishedEventArgs e) + { + var session = (sender as DTLSSession)!; + if (!e.Successful) + { + this.cache.Remove(session.Remote); + } + } + private void DecryptedReceived(object? _, DataReceivedEventArgs e) { this.DataReceived?.Invoke(this, e); diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs index a79792d..6506217 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -18,11 +18,6 @@ public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength) this.maxPacketLength = maxPacketLength; } - public Task WaitForMessageAsync(TimeSpan timeout, CancellationToken ct) - { - return this.sema.WaitAsync(timeout, ct); - } - public EndPoint Remote { get; } public int GetReceiveLimit() @@ -74,4 +69,4 @@ internal void Enqueue(ReadOnlySpan payload) this.messages.Enqueue(payload.ToArray()); this.sema.Release(); } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 298f70a..68a66f0 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -9,6 +9,7 @@ + diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs new file mode 100644 index 0000000..987b364 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs @@ -0,0 +1,65 @@ +namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration +{ + using FluentAssertions; + using WorldDirect.CoAP.Server.Extensions.Configuration; + using Xunit; + + public class BindingAddressSpecs + { + + + private readonly int validPort = 5684; + private readonly string coapScheme = "coap"; + private readonly string coapsScheme = "coaps"; + private readonly string validBindingAddress; + public BindingAddressSpecs() + { + this.validBindingAddress = $"{coapScheme}://*:{validPort}/"; + } + + [Fact] + public void RecognizesSchemeCorrectly() + { + var bindingAddress = BindingAddress.Parse(this.validBindingAddress); + bindingAddress.Scheme.Should().Be(this.coapScheme); + } + + [Fact] + public void RecognizesPortCorrectly() + { + var bindingAddress = BindingAddress.Parse(this.validBindingAddress); + bindingAddress.Port.Should().Be(this.validPort); + } + + [Fact] + public void RecognizesHostCorrectly() + { + var bindingAddress = BindingAddress.Parse(this.validBindingAddress); + bindingAddress.Host.Should().Be("*"); + } + + [Fact] + public void RecognizesLocalhostCorrectly() + { + var address = "coap://localhost:1234/"; + var bindingAddress = BindingAddress.Parse(address); + bindingAddress.Host.Should().Be("localhost"); + } + + [Fact] + public void RecognizesCoAPPortCorrectly() + { + var address = "coap://localhost"; + var bindingAddress = BindingAddress.Parse(address); + bindingAddress.Port.Should().Be(5683); + } + + [Fact] + public void RecognizesCoAPSPortCorrectly() + { + var address = "coaps://localhost"; + var bindingAddress = BindingAddress.Parse(address); + bindingAddress.Port.Should().Be(5684); + } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs new file mode 100644 index 0000000..0eaa1b3 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs @@ -0,0 +1,174 @@ +namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration +{ + using System.Collections.Generic; + using FluentAssertions; + using Microsoft.Extensions.Configuration; + using WorldDirect.CoAP.Server.Extensions.Configuration; + using Xunit; + + public class CertificationConfigSpecs + { + private readonly string pathConfigValue = "path123"; + private readonly string keyPathConfigValue = "keyPath123"; + private readonly string passwordConfigValue = "password123"; + private readonly string subjectConfigValue = "subject123"; + private readonly string storeConfigValue = "store123"; + private readonly string locationConfigValue = "location123"; + private readonly IConfiguration exampleConfiguration; + + private CertificateConfig certificateConfig; + + public CertificationConfigSpecs() + { + var exampleDict = new Dictionary() + { + { "Path", pathConfigValue }, + { "KeyPath", keyPathConfigValue }, + { "Password", passwordConfigValue}, + { "Subject", subjectConfigValue }, + { "Store", storeConfigValue }, + { "Location", locationConfigValue}, + { "AllowInvalid", "true" } + }; + this.exampleConfiguration = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(this.exampleConfiguration); + } + + [Fact] + public void AssignConfigurationCorrectly() + { + this.certificateConfig.Configuration.Should().Be(this.exampleConfiguration); + } + + [Fact] + public void ExtractsPathCorrectly() + { + this.certificateConfig.Path.Should().Be(this.pathConfigValue); + } + + [Fact] + public void ExtractsKeyPathCorrectly() + { + this.certificateConfig.KeyPath.Should().Be(this.keyPathConfigValue); + } + + [Fact] + public void ExtractsPasswordCorrectly() + { + this.certificateConfig.Password.Should().Be(this.passwordConfigValue); + } + + [Fact] + public void ExtractsSubjectCorrectly() + { + this.certificateConfig.Subject.Should().Be(this.subjectConfigValue); + } + + [Fact] + public void ExtractsStoreCorrectly() + { + this.certificateConfig.Store.Should().Be(this.storeConfigValue); + } + + [Fact] + public void ExtractsLocationCorrectly() + { + this.certificateConfig.Location.Should().Be(this.locationConfigValue); + } + + [Fact] + public void ExtractsAllowInvalidCorrectly() + { + this.certificateConfig.AllowInvalid.Should().BeTrue(); + } + + [Fact] + public void CertificateIsFileWhenPathIsSet() + { + var exampleDict = new Dictionary() + { + { "Path", pathConfigValue }, + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.IsFile.Should().BeTrue(); + } + + [Fact] + public void CertificateIsNotFileWhenPathIsNotSet() + { + var exampleDict = new Dictionary(); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.IsFile.Should().BeFalse(); + } + + [Fact] + public void CertificateIsFromStoreWhenSubjectIsSet() + { + var exampleDict = new Dictionary() + { + { "Subject", subjectConfigValue }, + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.IsFromStore.Should().BeTrue(); + } + + [Fact] + public void LocationDefaultsToCurrentUser() + { + var exampleDict = new Dictionary(); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.Location.Should().Be("CurrentUser"); + } + + [Fact] + public void AllowInvalidIsFalsePerDefault() + { + var exampleDict = new Dictionary() + { + }; + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.AllowInvalid.Should().BeFalse(); + } + + [Fact] + public void CertificateIsFromStoreWhenSubjectIsNotSet() + { + var exampleDict = new Dictionary(); + var config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + + this.certificateConfig = new CertificateConfig(config); + + this.certificateConfig.IsFromStore.Should().BeFalse(); + } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs new file mode 100644 index 0000000..78501f3 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs @@ -0,0 +1,86 @@ +namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration +{ + using System; + using System.Collections.Generic; + using System.Linq; + using FluentAssertions; + using Microsoft.Extensions.Configuration; + using WorldDirect.CoAP.Server.Extensions.Configuration; + using Xunit; + + public class ConfigurationReaderSpecs + { + private readonly Dictionary exampleDict; + private readonly string exampleUrl = "coaps://localhost:5684"; + private readonly string examplePath = "example.pfx"; + private readonly string exampleName = "CoAPSWithCertAuth"; + private readonly TimeSpan exampleTimeout = TimeSpan.FromSeconds(20); + private readonly string exampleBaseKey; + private IConfiguration config; + private ConfigurationReader reader; + public ConfigurationReaderSpecs() + { + this.exampleBaseKey = $"Endpoints:{this.exampleName}"; + this.exampleDict = new Dictionary() + { + { $"{exampleBaseKey}:Url", this.exampleUrl}, + { $"{exampleBaseKey}:Certificate:Path", this.examplePath}, + { $"{exampleBaseKey}:ClientCA:0:Path", this.examplePath}, + { $"{exampleBaseKey}:ClientCA:1:Path", this.examplePath}, + { $"{exampleBaseKey}:HandshakeTimeout", this.exampleTimeout.ToString()}, + + }; + this.config = new ConfigurationBuilder() + .AddInMemoryCollection(exampleDict) + .Build(); + this.reader = new ConfigurationReader(this.config); + } + + [Fact] + public void ReadsOneEndpoint() + { + var endpoints = this.reader.Endpoints; + endpoints.Should().HaveCount(1); + } + + [Fact] + public void ReadsNameCorrectly() + { + var endpoints = this.reader.Endpoints; + var endpoint = endpoints.Single(); + endpoint.Name.Should().Be(this.exampleName); + } + + [Fact] + public void ReadsUrlCorrectly() + { + var endpoints = this.reader.Endpoints; + var endpoint = endpoints.Single(); + endpoint.Url.Should().Be(this.exampleUrl); + } + + [Fact] + public void ReadsTwoClientCAs() + { + var endpoints = this.reader.Endpoints; + var endpoint = endpoints.Single(); + endpoint.ClientCA.Should().HaveCount(2); + } + + [Fact] + public void ReadsCertificateConfig() + { + var endpoints = this.reader.Endpoints; + var endpoint = endpoints.Single(); + endpoint.CertificateConfig.Should().NotBeNull(); + } + + [Fact] + public void ReadsHandshakeTimeoutCorrectly() + { + var endpoints = this.reader.Endpoints; + var endpoint = endpoints.Single(); + endpoint.HandshakeTimeout.Should().Be(exampleTimeout); + } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj b/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj new file mode 100644 index 0000000..7379746 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + Always + + + + diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs new file mode 100644 index 0000000..817d5c3 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs @@ -0,0 +1,33 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration +{ + using Microsoft.Extensions.Configuration; + + public class CertificateConfig + { + public CertificateConfig(IConfiguration configuration) + { + this.Configuration = configuration; + this.Path = configuration[nameof(this.Path)]; + this.KeyPath = configuration[nameof(this.KeyPath)]; + this.Password = configuration[nameof(this.Password)]; + this.Subject = configuration[nameof(this.Subject)]; + this.Store = configuration[nameof(this.Store)]; + this.Location = configuration[nameof(this.Location)] == null ? "CurrentUser" : configuration[nameof(this.Location)]; + this.AllowInvalid = configuration[nameof(this.AllowInvalid)] != null && bool.Parse(configuration[nameof(this.AllowInvalid)]); + } + + public IConfiguration Configuration { get; } + + 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; } + public bool AllowInvalid { get; set; } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs new file mode 100644 index 0000000..47e5aee --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs @@ -0,0 +1,152 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net; + using Microsoft.Extensions.Configuration; + + public class ListenOption + { + + public ListenOption(EndPoint endpoint, EndpointConfig endpointConfig) + { + this.Endpoint = endpoint; + this.EndpointConfig = endpointConfig; + } + + public EndPoint Endpoint { get; set; } + public EndpointConfig EndpointConfig { get; set; } + } + + public class CoAPServerOptions + { + + internal CoAPServerOptions(IEnumerable listenOptions) + { + this.ListenOptions = listenOptions; + } + + public IEnumerable ListenOptions { get; } + + } + + /// + /// 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 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); + } + } + + public class CoAPServerOptionsLoader + { + private readonly IConfiguration config; + + public CoAPServerOptionsLoader(IConfiguration config) + { + this.config = config; + } + + public CoAPServerOptions Options => this.Build(); + + private CoAPServerOptions Build() + { + var reader = new ConfigurationReader(this.config); + var endpoints = reader.Endpoints; + + var listenOptions = new List(); + + foreach (var endpoint in endpoints) + { + var address = BindingAddress.Parse(endpoint.Url); + if (address.Host == "localhost") + { + listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Loopback, address.Port), endpoint)); + } + else + { + listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Any, address.Port), endpoint)); + } + } + + return new CoAPServerOptions(listenOptions); + } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs new file mode 100644 index 0000000..75be45b --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs @@ -0,0 +1,124 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration +{ + using System; + using System.Collections.Generic; + using Microsoft.Extensions.Configuration; + + /* + * "CoAP": { + * "Endpoints": { + * "CoAPSWithCertAuth": { + * "Url": "coaps://*:5684", + * "ClientAuthenticationMode": "Certificate", + * "Certificate": { + * ... + * } + * } + * } + * } + * + */ + + /// + /// + /// + /// + /// Based on Kestrel: + /// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L67 + /// + public class ConfigurationReader + { + private const string EndpointsKey = "Endpoints"; + private const string UrlKey = "Url"; + private const string CertificateKey = "Certificate"; + private const string ClientCAKey = "ClientCA"; + private const string HandshakeTimeout = "HandshakeTimeout"; + private readonly IConfiguration config; + + public ConfigurationReader(IConfiguration config) + { + this.config = config; + } + + public IEnumerable Endpoints => this.ReadEndpoints(); + + private IEnumerable ReadEndpoints() + { + var endpoints = new List(); + var endpointConfig = this.config.GetSection(EndpointsKey); + var endpointsConfigurations = endpointConfig.GetChildren(); + foreach (var endpointCfg in endpointsConfigurations) + { + var url = endpointCfg[UrlKey]; + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException($"Url of endpoint {endpointCfg.Key} must be defined."); + } + + var caSections = endpointCfg.GetSection(ClientCAKey).GetChildren(); + var cas = caSections.Select(section => new CertificateConfig(section)).ToArray(); + CertificateConfig? certificateConfig = null; + if (endpointCfg.GetSection(CertificateKey).GetChildren().Any()) + { + certificateConfig = new CertificateConfig(endpointCfg.GetSection(CertificateKey)); + } + var endpoint = new EndpointConfig(endpointCfg.Key, url) + { + CertificateConfig = certificateConfig, + ClientCA = cas, + HandshakeTimeout = endpointCfg.GetSection(HandshakeTimeout).Get(), + }; + + endpoints.Add(endpoint); + } + + return endpoints; + } + + private ClientAuthenticationMode ReadClientAuthenticationMode(string? value) + { + if (string.IsNullOrEmpty(value)) + { + return ClientAuthenticationMode.NoAuthentication; + } + + if(Enum.TryParse(value,true, out var mode)) + { + return mode; + } + + throw new InvalidOperationException($"Unknown ClientAuthenticationMode was selected {value}"); + } + } + + public enum ClientAuthenticationMode + { + NoAuthentication, + Certificate, + PSK, + CertificateOrPSK, + } + + /// + /// + /// + /// + /// Based on Kestrel: + /// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L267 + /// + public class EndpointConfig + { + + public EndpointConfig(string name, string url) + { + this.Name = name; + this.Url = url; + } + + public string Name { get; set; } + public string Url { get; set; } + public CertificateConfig? CertificateConfig { get; set; } + public CertificateConfig[]? ClientCA { get; set; } + public TimeSpan HandshakeTimeout { get; set; } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions/README.md b/WorldDirect.CoAP.Server.Extensions/README.md new file mode 100644 index 0000000..5feeb24 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/README.md @@ -0,0 +1,66 @@ +# Future improvements + +- Enable RSA Certificates +- Load own Certificate Chain from store +- PSK client authorization + +# Examples + +## CoAP Server Config Unsecure +``` + "Coap": { + "Endpoints": { + "CoAP": { + "Url": "coap://*:5683" + } + } + } +``` + +## CoAPS Server Config with Certificate from pfx file without client authentication +``` + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Path": "server.pfx", + "Password": "$CREDENTIAL_PLACEHOLDER$" + } + } + } + } +``` + +## CoAPS Server Config with Certificate from .pem and encrypted .key file without client authentication +``` + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Path": "server-cert.pem", + "KeyPath": "server-key.key" + "Password": "$CREDENTIAL_PLACEHOLDER$" + } + } + } + } +``` + +## CoAPS Server Config with Certificate from store without client authentication +``` + "Coap": { + "Endpoints": { + "CoAPS": { + "Url": "coaps://*:5684", + "Certificate": { + "Subject": "ls1.argus.dev.energy.loc", + "Store": "", + "Location": "", + "AllowInvalid": "" + } + } + } + } +``` diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs new file mode 100644 index 0000000..8e598b1 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,518 @@ +namespace WorldDirect.CoAP.Server.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Threading.Tasks; + using Channel; + using Configuration; + using DTLS; + using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Net; + using Org.BouncyCastle.Asn1.Cmp; + using Org.BouncyCastle.Asn1.X509; + using Org.BouncyCastle.Crypto; + using Org.BouncyCastle.Crypto.Parameters; + using Org.BouncyCastle.Ocsp; + using Org.BouncyCastle.OpenSsl; + using Org.BouncyCastle.Pkcs; + using Org.BouncyCastle.Security; + using Org.BouncyCastle.Tls; + using Org.BouncyCastle.Tls.Crypto.Impl.BC; + using Org.BouncyCastle.X509; + + 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; + } + } + + internal class InMemoryPasswordFinder : IPasswordFinder + { + private readonly string password; + public InMemoryPasswordFinder(string password) + { + this.password = password; + } + public char[] GetPassword() + { + return this.password.ToCharArray(); + } + } + + public class DTLSServerBuilder + { + // TODO: Check if certificate usage is allowed for server auth when loaded from files + // TODO: Check if CA is allowed to be used for (KeyCertSign) when loaded from file + private readonly BcTlsCrypto crypto; + private readonly DTLSServerConfig config; + public DTLSServerBuilder() + { + this.crypto = new BcTlsCrypto(new SecureRandom()); + this.config = new DTLSServerConfig(); + } + + public DTLSServerBuilder AddCertificate(CertificateConfig config) + { + // todo check if multiple certificates are valid for same host + // see DTLSServer FindCertificate Function for more information + if (config.IsFromStore) + { + var ecdasCert = this.LoadCertAndKeyFromStore(config); + this.config.EcCertificates.Add(ecdasCert); + } + else if (config.IsFile) + { + try + { + // *.pem and *.key file + if (!string.IsNullOrEmpty(config.Path) && !string.IsNullOrEmpty(config.KeyPath)) + { + var ecdsaCert = this.LoadCertAndKeyFromFiles(config); + this.config.EcCertificates.Add(ecdsaCert); + } + // pfx file + else if (!string.IsNullOrEmpty(config.Path)) + { + var ecdsaCert = this.LoadCertAndKeyFromPfxFile(config); + this.config.EcCertificates.Add(ecdsaCert); + } + else + { + throw new InvalidOperationException("Invalid configuration for certificate"); + } + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load certificate file {config.Path}", e); + } + } + else + { + throw new InvalidOperationException($"Invalid configuration for certificate"); + } + this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8); + this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + + return this; + } + + public DTLSServerBuilder AddCA(CertificateConfig config) + { + if (config.IsFromStore) + { + var cert = this.LoadCAFromStore(config); + this.config.CAs.Add(cert); + } + else if (config.IsFile) + { + try + { + // pem file + if (!string.IsNullOrEmpty(config.Path)) + { + var cert = this.LoadCertFromFile(config.Path!); + this.config.CAs.Add(cert); + } + else + { + throw new InvalidOperationException("Invalid configuration for CA certificate"); + } + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load CA certificate file {config.Path}", e); + } + } + else + { + throw new InvalidOperationException($"Invalid configuration for CA certificate"); + } + return this; + } + + public DTLSServerBuilder SetHandshakeTimeout(TimeSpan timeout) + { + this.config.HandshakeTimeout = timeout; + return this; + } + + + public DTLSServer Build() + { + return new DTLSServer(this.crypto, this.config); + } + + 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(CertificateConfig 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)); + + object keyObj = pemReader.ReadObject(); + + if (keyObj.GetType() != typeof(ECPrivateKeyParameters)) + { + throw new InvalidOperationException($"{config.KeyPath} does not store a EC key. Currently only ECDSA is supported"); + } + + var key = keyObj 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, key!); + return ecCert; + } + + private EcServerCertificate LoadCertAndKeyFromPfxFile(CertificateConfig 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(CertificateConfig 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(CertificateConfig config) + { + var cert = CertificateManager.LoadCAFromStore(config.Store!, config.Location!, config.Subject!, config.AllowInvalid); + return this.ToBcCertificate(cert); + } + + private Org.BouncyCastle.X509.X509Certificate ToBcCertificate(X509Certificate2 cert) + { + return new Org.BouncyCastle.X509.X509Certificate(cert.Export(X509ContentType.Cert)); + } + + 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; + } + } + + internal class DTLSFactory : IDTLSFactory + { + private readonly DTLSServerBuilder builder; + + public DTLSFactory(DTLSServerBuilder builder) + { + this.builder = builder; + } + public DTLSServer CreateServer() + { + return this.builder.Build(); + } + } + + public static class ServiceProviderExtensions + { + + /// + /// Requires an in the service provider. + /// + /// + /// + /// + public static IServiceCollection ConfigureCoAPServer(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(serviceProvider => Configure(serviceProvider, configuration)); + + return services; + } + + private static CoapServer Configure(IServiceProvider serviceProvider, IConfiguration configuration) + { + var server = new CoapServer(); + + + var loader = new CoAPServerOptionsLoader(configuration); + var options = loader.Options; + + foreach (var listenEndpoint in options.ListenOptions) + { + if (listenEndpoint!.EndpointConfig.CertificateConfig == null) + { + // unsecure + server.AddEndPoint(listenEndpoint.Endpoint as IPEndPoint); + } + else + { + var dtlsServerBuilder = new DTLSServerBuilder() + .AddCertificate(listenEndpoint.EndpointConfig.CertificateConfig) + .SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); + + if (listenEndpoint.EndpointConfig.ClientCA != null && listenEndpoint.EndpointConfig.ClientCA.Length > 0) + { + foreach (var ca in listenEndpoint.EndpointConfig.ClientCA) + { + dtlsServerBuilder.AddCA(ca); + } + } + + var channel = new UDPChannel(listenEndpoint.Endpoint); + var config = CoapConfig.Default; + channel.ReceiveBufferSize = config.ChannelReceiveBufferSize; + channel.SendBufferSize = config.ChannelSendBufferSize; + channel.ReceivePacketSize = config.ChannelReceivePacketSize; + + var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), new DTLSFactory(dtlsServerBuilder), channel); + + server.AddEndPoint(ep); + } + } + + + + return server; + } + } +} diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj new file mode 100644 index 0000000..2d9f177 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + From 0b4801a09ad1239a75967b4a62a0440bce34385c Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 25 Apr 2023 10:40:28 +0200 Subject: [PATCH 03/27] add documentation --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 12 +- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 42 +- .../DTLSClientAuthentication.cs | 40 ++ .../DTLSDataReceivedEventArgs.cs | 50 ++ WorldDirect.CoAP.DTLS/DTLSServer.cs | 54 ++- WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs | 34 +- WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 16 +- WorldDirect.CoAP.DTLS/DTLSSession.cs | 25 +- WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs | 11 +- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 8 +- WorldDirect.CoAP.DTLS/EcServerCertificate.cs | 16 +- .../HandshakeFinishedEventArgs.cs | 12 + WorldDirect.CoAP.DTLS/IDTLSFactory.cs | 9 +- WorldDirect.CoAP.DTLS/IUDPSender.cs | 10 +- WorldDirect.CoAP.DTLS/UdpChannelSender.cs | 11 +- WorldDirect.CoAP.DTLS/UdpTransport.cs | 51 ++ .../CertificateManager.cs | 150 ++++++ .../Configuration/BindingAddress.cs | 87 ++++ .../Configuration/CoAPServerOptions.cs | 136 ------ .../Configuration/CoAPServerOptionsLoader.cs | 39 ++ .../Configuration/ConfigurationReader.cs | 64 +++ .../Configuration/EndpointConfig.cs | 101 +--- .../Configuration/ListenOption.cs | 16 + .../CryptographyExtensions.cs | 11 + .../DTLSFactory.cs | 17 + .../DTLSServerBuilder.cs | 267 +++++++++++ .../InMemoryPasswordFinder.cs | 16 + WorldDirect.CoAP.Server.Extensions/README.md | 36 +- .../ServiceProviderExtensions.cs | 449 +----------------- WorldDirect.CoAP/Net/CoAPEndPoint.cs | 2 + WorldDirect.CoAP/Net/IEndPoint.cs | 4 + WorldDirect.CoAP/Request.cs | 2 +- .../Server/Resources/CoapExchange.cs | 5 + 33 files changed, 1080 insertions(+), 723 deletions(-) create mode 100644 WorldDirect.CoAP.DTLS/DTLSClientAuthentication.cs create mode 100644 WorldDirect.CoAP.DTLS/DTLSDataReceivedEventArgs.cs create mode 100644 WorldDirect.CoAP.DTLS/HandshakeFinishedEventArgs.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/CertificateManager.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs create mode 100644 WorldDirect.CoAP.Server.Extensions/InMemoryPasswordFinder.cs diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 53a84d1..1858d26 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -39,6 +39,9 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox private IExecutor _executor; private DTLSChannel channel; + /// + public string Scheme => CoapConstants.SecureUriScheme; + /// public event EventHandler> SendingRequest; /// @@ -66,7 +69,7 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory) channel.SendBufferSize = this._config.ChannelSendBufferSize; channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; this.channel = new DTLSChannel(channel, cache, factory); - this.channel.DataReceived += Channel_DataReceived; + this.channel.DtlsDataReceived += Channel_DataReceived; } public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel) @@ -75,7 +78,7 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channe _matcher = new Matcher(this._config); _coapStack = new CoapStack(this._config); this.channel = new DTLSChannel(channel, cache, factory); - this.channel.DataReceived += Channel_DataReceived; + this.channel.DtlsDataReceived += Channel_DataReceived; } @@ -194,7 +197,7 @@ public void SendEmptyMessage(Exchange exchange, EmptyMessage message) } - private void Channel_DataReceived(object? sender, DataReceivedEventArgs e) + private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) { IMessageDecoder decoder = Spec.NewMessageDecoder(e.Data); if (decoder.IsRequest) @@ -203,6 +206,7 @@ private void Channel_DataReceived(object? sender, DataReceivedEventArgs e) try { request = decoder.DecodeRequest(); + request.EndPoint = this; } catch (Exception) { @@ -237,7 +241,7 @@ private void Channel_DataReceived(object? sender, DataReceivedEventArgs e) if (exchange != null) { exchange.EndPoint = this; - //exchange.Set(nameof(DTLSClient), e.Remote); + exchange.Set(nameof(DTLSClientAuthentication), e.ClientAuthentication); _coapStack.ReceiveRequest(exchange, request); } } diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index cf018bc..ecc52cb 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -9,52 +9,86 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; + /// + /// Represents the dtls channel for a coap communication. + /// public class DTLSChannel : IChannel { private readonly UDPChannel channel; private readonly DTLSSessionManager sessionManager; - public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory) + /// + /// Initializes a new instance of the class. + /// + /// The underlying udp channel used to send/receive data. + /// The cache to store dtls sessions. + /// The factory to create dtls server. + /// The timeout after which a session is deleted. + public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory, TimeSpan sessionTimeout) { this.channel = channel; this.channel.DataReceived += DtlsReceived; - // todo configure sessiontimeout - var config = new DTLSSessionConfig() {MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = TimeSpan.FromMinutes(3),}; + var config = new DTLSSessionConfig() { MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = sessionTimeout, }; this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), factory, config); } + /// + /// Initializes a new instance of the class. + /// + /// The underlying udp channel used to send/receive data. + /// The cache to store dtls sessions. + /// The factory to create dtls server. + public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory) + : this(channel, cache, factory, TimeSpan.FromMinutes(2)) + { + } + private void DtlsReceived(object? sender, DataReceivedEventArgs e) { this.sessionManager.ReceivedUdpPacket(e.Data, e.EndPoint); } + /// 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) { this.sessionManager.SendTo(data, ep); } - private void DecryptedForwarding(object? sender, DataReceivedEventArgs e) + private void DecryptedForwarding(object? sender, DTLSDataReceivedEventArgs e) { 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/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index 23f3599..45ad96f 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -1,41 +1,75 @@ namespace WorldDirect.CoAP.DTLS; +using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Pkix; using Org.BouncyCastle.Tls; using Org.BouncyCastle.Tls.Crypto; 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 cryptostack. + /// The configuration of the server. public DTLSServer(BcTlsCrypto crypto, DTLSServerConfig config) : base(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 => this.m_context.SecurityParameters.PeerCertificate.IsEmpty ? null : this.m_context.SecurityParameters.PeerCertificate.GetCertificateAt(0); + /// + /// 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(); } + /// + /// 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.Count == 0) + if (this.config.CA == null) { return null; } @@ -46,13 +80,18 @@ public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); // send back a list of supported CAs - var authorities = this.config.CAs.Select(ca => ca.SubjectDN).ToList(); + var authorities = new List() {this.config.CA.SubjectDN}; 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; @@ -65,6 +104,10 @@ public override TlsCredentials GetCredentials() } } + /// + /// Handling of the reported client certificate. + /// + /// The certificate of the client. public override void NotifyClientCertificate(Certificate clientCertificate) { if (clientCertificate.IsEmpty) @@ -74,13 +117,14 @@ public override void NotifyClientCertificate(Certificate clientCertificate) var chain = clientCertificate.GetCertificateList()!; var chainAsCertificate = chain.Select(c => new X509Certificate(c.GetEncoded())).ToArray(); - var trustAnchors = this.config.CAs.Select(ca => new TrustAnchor(ca, null)); + var trustAnchor = new TrustAnchor(this.config.CA, null); - var parameters = new PkixParameters(new SortedSet(trustAnchors)); + var parameters = new PkixParameters(new SortedSet() { trustAnchor }); parameters.IsRevocationEnabled = false; var path = new PkixCertPath(chainAsCertificate); var validator = new PkixCertPathValidator(); validator.Validate(path, parameters); + this.IsAuthenticated = true; } private TlsCredentialedSigner GetECDsaSignerCredentials() @@ -111,6 +155,6 @@ private EcServerCertificate FindCertificate(IList names) // 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.EcCertificates.First(); + return this.config.EcCertificate!; } } diff --git a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs index 53129ee..1471034 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs @@ -10,7 +10,7 @@ using Org.BouncyCastle.X509; /// -/// +/// A helper class to configure the . /// /// /// Limitations: @@ -24,12 +24,21 @@ public class DTLSServerBuilder private readonly DTLSServerConfig config; + /// + /// Initializes a new instance of the class. + /// public DTLSServerBuilder() { this.crypto = new BcTlsCrypto(new SecureRandom()); this.config = new DTLSServerConfig(); } + /// + /// Add a pkcs12 store where the certificate will be loaded from. + /// + /// Path to the file. + /// Password of the file. + /// The builder. public DTLSServerBuilder WithStore(string file, string password) { var store = new Pkcs12StoreBuilder().Build(); @@ -39,6 +48,11 @@ public DTLSServerBuilder WithStore(string file, string password) return this; } + /// + /// Set the timeout of the handshake. + /// + /// The timeout. + /// The builder. public DTLSServerBuilder WithHandShakeTimeout(TimeSpan timeout) { this.config.HandshakeTimeout = timeout; @@ -74,11 +88,7 @@ public DTLSServerBuilder WithEcdsaCertificate(string alias) var ecPrivateKey = (key.Key as ECPrivateKeyParameters)!; var ecdsaCertificate = new Certificate(x509Certs.ToArray()); - this.config.EcCertificates.Add(new EcServerCertificate(ecdsaCertificate, ecPrivateKey)); - if (this.config.EcCertificates.Count > 1) - { - throw new InvalidOperationException("Currently one one certificate is supported."); - } + this.config.EcCertificate = new EcServerCertificate(ecdsaCertificate, ecPrivateKey); return this; } @@ -98,10 +108,16 @@ public DTLSServerBuilder WithTrustedRoot(string filename) { throw new InvalidOperationException($"Could not read certificate from {filename}"); } - this.config.CAs.Add(cert); + + this.config.CA = cert; return this; } + /// + /// Adds enabled ciphersuites to the server. + /// + /// The cipher suites to add. + /// The builder public DTLSServerBuilder WithCipherSuites(IEnumerable suites) { this.config.CipherSuites.AddRange(suites); @@ -109,6 +125,10 @@ public DTLSServerBuilder WithCipherSuites(IEnumerable suites) return this; } + /// + /// Builds the based on the configuration. + /// + /// The configured . public DTLSServer Build() { return new DTLSServer(this.crypto, this.config); diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs index a2f138a..0580d88 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -1,11 +1,23 @@ namespace WorldDirect.CoAP.DTLS; +/// +/// Represents the configuration of the . +/// public class DTLSServerConfig { - public List EcCertificates { get; set; } = new(); + /// + /// Gets or sets the certificate of the server. + /// + public EcServerCertificate? EcCertificate { get; set; } - public List CAs { get; set; } = new (); + /// + /// Gets or sets the CA to authorize the connecting clients. + /// + public Org.BouncyCastle.X509.X509Certificate? CA { get; set; } + /// + /// Gets or sets the available cipher suites. + /// public List CipherSuites { get; set; } = new(); /// diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 4f080f0..49bd472 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -1,14 +1,10 @@ namespace WorldDirect.CoAP.DTLS; using System.Net; +using System.Security.Cryptography.X509Certificates; using Channel; using Org.BouncyCastle.Tls; -internal class HandshakeFinishedEventArgs : EventArgs -{ - public bool Successful { get; set; } -} - internal class DTLSSession { private readonly CancellationTokenSource cts; @@ -34,8 +30,11 @@ public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, Cancel /// /// An event when a new decrypted payload was received. /// - public event EventHandler? DataReceived; + public event EventHandler? DataReceived; + /// + /// An event when the handshake was finished. + /// public event EventHandler? HandshakeFinished; /// @@ -87,13 +86,21 @@ public void Enqueue(ReadOnlySpan payload) } else if (length > 0) { - this.DataReceived?.Invoke(this, new DataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote)); + if (this.dtlsServer.IsAuthenticated) + { + if (this.dtlsServer.PeerCertificate != null) + { + var peerCert = new X509Certificate(this.dtlsServer.PeerCertificate.GetEncoded()); + this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, peerCert)); + } + } + throw new NotImplementedException($"PSK or unauthenticated communication is not implemented"); } } } - private async Task HandleSession() + private Task HandleSession() { try { @@ -115,5 +122,7 @@ private async Task HandleSession() { this.HandshakeFinished?.Invoke(this, new HandshakeFinishedEventArgs() { Successful = !this.HandshakeFailed }); } + + return Task.CompletedTask; } } diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs index 9ea5760..6d0cd94 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs @@ -1,8 +1,17 @@ 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; } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 85fc2a0..e6e9551 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -14,7 +14,6 @@ /// public class DTLSSessionManager { - // todo X509Certificate dotnet forwarding private readonly IMemoryCache cache; private readonly IUDPSender sender; private readonly IDTLSFactory factory; @@ -38,7 +37,7 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, IDTLSFactory fa /// /// An event to notify listener a new decrypted udp packet was received. /// - public event EventHandler? DataReceived; + public event EventHandler? DataReceived; /// /// Send a udp packet encrypted to the remote endpoint. @@ -53,6 +52,9 @@ public void SendTo(ReadOnlySpan packet, EndPoint endPoint) } } + /// + /// Stops the manager. + /// public void Stop() { this.cts.Cancel(); @@ -93,7 +95,7 @@ private void HandshakeFinished(object? sender, HandshakeFinishedEventArgs e) } } - private void DecryptedReceived(object? _, DataReceivedEventArgs e) + private void DecryptedReceived(object? _, DTLSDataReceivedEventArgs e) { this.DataReceived?.Invoke(this, e); } diff --git a/WorldDirect.CoAP.DTLS/EcServerCertificate.cs b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs index 2b67fd8..92460c4 100644 --- a/WorldDirect.CoAP.DTLS/EcServerCertificate.cs +++ b/WorldDirect.CoAP.DTLS/EcServerCertificate.cs @@ -3,15 +3,29 @@ 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; } -} \ No newline at end of file +} 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/IDTLSFactory.cs b/WorldDirect.CoAP.DTLS/IDTLSFactory.cs index e70b23b..5981e87 100644 --- a/WorldDirect.CoAP.DTLS/IDTLSFactory.cs +++ b/WorldDirect.CoAP.DTLS/IDTLSFactory.cs @@ -1,6 +1,13 @@ namespace WorldDirect.CoAP.DTLS; +/// +/// An interface to create a . +/// public interface IDTLSFactory { + /// + /// Create a new instance. + /// + /// The newly created . public DTLSServer CreateServer(); -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/IUDPSender.cs b/WorldDirect.CoAP.DTLS/IUDPSender.cs index 3a9d010..bde2d12 100644 --- a/WorldDirect.CoAP.DTLS/IUDPSender.cs +++ b/WorldDirect.CoAP.DTLS/IUDPSender.cs @@ -2,7 +2,15 @@ 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); -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs index 132e3ee..237a91d 100644 --- a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs +++ b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs @@ -3,16 +3,25 @@ using System.Net; using Channel; +/// +/// Represents a udp sender using a CoAP UDP channel. +/// public class UdpChannelSender : IUDPSender { private readonly UDPChannel channel; + /// + /// 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.channel.Send(payload.ToArray(), remote); } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs index 6506217..2c7ada6 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -4,6 +4,9 @@ using System.Net; using Org.BouncyCastle.Tls; +/// +/// Represents the udp package buffer of a DTLS connection. +/// internal class UdpTransport : DatagramTransport { private readonly IUDPSender sender; @@ -11,6 +14,12 @@ internal class UdpTransport : DatagramTransport private readonly ConcurrentQueue messages = new (); private readonly SemaphoreSlim sema = new (0); + /// + /// Initialize a new instance of the class. + /// + /// The implementation of the udp + /// The endpoint of the connection. + /// The maximum length of a dtls package. public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength) { this.Remote = remote; @@ -18,18 +27,39 @@ public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength) this.maxPacketLength = maxPacketLength; } + /// + /// 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.sema.Wait(TimeSpan.FromMilliseconds(waitMillis))) @@ -44,26 +74,47 @@ public int Receive(Span buffer, int waitMillis) 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) { 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) { this.messages.Enqueue(payload.ToArray()); diff --git a/WorldDirect.CoAP.Server.Extensions/CertificateManager.cs b/WorldDirect.CoAP.Server.Extensions/CertificateManager.cs new file mode 100644 index 0000000..ba6d233 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/CertificateManager.cs @@ -0,0 +1,150 @@ +namespace WorldDirect.CoAP.Server.Extensions; + +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; + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs new file mode 100644 index 0000000..55bcf86 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs @@ -0,0 +1,87 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration; + +using System.Globalization; + +/// +/// 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 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); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs index 47e5aee..894adba 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs @@ -1,23 +1,6 @@ namespace WorldDirect.CoAP.Server.Extensions.Configuration { - using System; using System.Collections.Generic; - using System.Globalization; - using System.Net; - using Microsoft.Extensions.Configuration; - - public class ListenOption - { - - public ListenOption(EndPoint endpoint, EndpointConfig endpointConfig) - { - this.Endpoint = endpoint; - this.EndpointConfig = endpointConfig; - } - - public EndPoint Endpoint { get; set; } - public EndpointConfig EndpointConfig { get; set; } - } public class CoAPServerOptions { @@ -30,123 +13,4 @@ internal CoAPServerOptions(IEnumerable listenOptions) public IEnumerable ListenOptions { get; } } - - /// - /// 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 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); - } - } - - public class CoAPServerOptionsLoader - { - private readonly IConfiguration config; - - public CoAPServerOptionsLoader(IConfiguration config) - { - this.config = config; - } - - public CoAPServerOptions Options => this.Build(); - - private CoAPServerOptions Build() - { - var reader = new ConfigurationReader(this.config); - var endpoints = reader.Endpoints; - - var listenOptions = new List(); - - foreach (var endpoint in endpoints) - { - var address = BindingAddress.Parse(endpoint.Url); - if (address.Host == "localhost") - { - listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Loopback, address.Port), endpoint)); - } - else - { - listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Any, address.Port), endpoint)); - } - } - - return new CoAPServerOptions(listenOptions); - } - } } diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs new file mode 100644 index 0000000..ba34394 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs @@ -0,0 +1,39 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration; + +using System.Net; +using Microsoft.Extensions.Configuration; + +public class CoAPServerOptionsLoader +{ + private readonly IConfiguration config; + + public CoAPServerOptionsLoader(IConfiguration config) + { + this.config = config; + } + + public CoAPServerOptions Options => this.Build(); + + private CoAPServerOptions Build() + { + var reader = new ConfigurationReader(this.config); + var endpoints = reader.Endpoints; + + var listenOptions = new List(); + + foreach (var endpoint in endpoints) + { + var address = BindingAddress.Parse(endpoint.Url); + if (address.Host == "localhost") + { + listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Loopback, address.Port), endpoint)); + } + else + { + listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Any, address.Port), endpoint)); + } + } + + return new CoAPServerOptions(listenOptions); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs new file mode 100644 index 0000000..a61c870 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs @@ -0,0 +1,64 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration; + +using Microsoft.Extensions.Configuration; + +/// +/// +/// +/// +/// Based on Kestrel: +/// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L67 +/// +public class ConfigurationReader +{ + private const string EndpointsKey = "Endpoints"; + private const string UrlKey = "Url"; + private const string CertificateKey = "Certificate"; + private const string ClientCAKey = "ClientCA"; + private const string HandshakeTimeoutKey = "HandshakeTimeout"; + private readonly IConfiguration config; + + public ConfigurationReader(IConfiguration config) + { + this.config = config; + } + + public IEnumerable Endpoints => this.ReadEndpoints(); + + private IEnumerable ReadEndpoints() + { + var endpoints = new List(); + var endpointConfig = this.config.GetSection(EndpointsKey); + var endpointsConfigurations = endpointConfig.GetChildren(); + foreach (var endpointCfg in endpointsConfigurations) + { + var url = endpointCfg[UrlKey]; + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException($"Url of endpoint {endpointCfg.Key} must be defined."); + } + + + CertificateConfig? certificateConfig = null; + if (endpointCfg.GetSection(CertificateKey).GetChildren().Any()) + { + certificateConfig = new CertificateConfig(endpointCfg.GetSection(CertificateKey)); + } + CertificateConfig? clientCAConfig = null; + if (endpointCfg.GetSection(ClientCAKey).GetChildren().Any()) + { + clientCAConfig = new CertificateConfig(endpointCfg.GetSection(ClientCAKey)); + } + var endpoint = new EndpointConfig(endpointCfg.Key, url) + { + CertificateConfig = certificateConfig, + ClientCA = clientCAConfig, + HandshakeTimeout = endpointCfg.GetSection(HandshakeTimeoutKey).Get(), + }; + + endpoints.Add(endpoint); + } + + return endpoints; + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs index 75be45b..6f4d50b 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs @@ -1,106 +1,9 @@ namespace WorldDirect.CoAP.Server.Extensions.Configuration { using System; - using System.Collections.Generic; - using Microsoft.Extensions.Configuration; - - /* - * "CoAP": { - * "Endpoints": { - * "CoAPSWithCertAuth": { - * "Url": "coaps://*:5684", - * "ClientAuthenticationMode": "Certificate", - * "Certificate": { - * ... - * } - * } - * } - * } - * - */ - - /// - /// - /// - /// - /// Based on Kestrel: - /// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L67 - /// - public class ConfigurationReader - { - private const string EndpointsKey = "Endpoints"; - private const string UrlKey = "Url"; - private const string CertificateKey = "Certificate"; - private const string ClientCAKey = "ClientCA"; - private const string HandshakeTimeout = "HandshakeTimeout"; - private readonly IConfiguration config; - - public ConfigurationReader(IConfiguration config) - { - this.config = config; - } - - public IEnumerable Endpoints => this.ReadEndpoints(); - - private IEnumerable ReadEndpoints() - { - var endpoints = new List(); - var endpointConfig = this.config.GetSection(EndpointsKey); - var endpointsConfigurations = endpointConfig.GetChildren(); - foreach (var endpointCfg in endpointsConfigurations) - { - var url = endpointCfg[UrlKey]; - if (string.IsNullOrEmpty(url)) - { - throw new InvalidOperationException($"Url of endpoint {endpointCfg.Key} must be defined."); - } - - var caSections = endpointCfg.GetSection(ClientCAKey).GetChildren(); - var cas = caSections.Select(section => new CertificateConfig(section)).ToArray(); - CertificateConfig? certificateConfig = null; - if (endpointCfg.GetSection(CertificateKey).GetChildren().Any()) - { - certificateConfig = new CertificateConfig(endpointCfg.GetSection(CertificateKey)); - } - var endpoint = new EndpointConfig(endpointCfg.Key, url) - { - CertificateConfig = certificateConfig, - ClientCA = cas, - HandshakeTimeout = endpointCfg.GetSection(HandshakeTimeout).Get(), - }; - - endpoints.Add(endpoint); - } - - return endpoints; - } - - private ClientAuthenticationMode ReadClientAuthenticationMode(string? value) - { - if (string.IsNullOrEmpty(value)) - { - return ClientAuthenticationMode.NoAuthentication; - } - - if(Enum.TryParse(value,true, out var mode)) - { - return mode; - } - - throw new InvalidOperationException($"Unknown ClientAuthenticationMode was selected {value}"); - } - } - - public enum ClientAuthenticationMode - { - NoAuthentication, - Certificate, - PSK, - CertificateOrPSK, - } /// - /// + /// The conf /// /// /// Based on Kestrel: @@ -118,7 +21,7 @@ public EndpointConfig(string name, string url) public string Name { get; set; } public string Url { get; set; } public CertificateConfig? CertificateConfig { get; set; } - public CertificateConfig[]? ClientCA { get; set; } + public CertificateConfig? ClientCA { get; set; } public TimeSpan HandshakeTimeout { get; set; } } } diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs new file mode 100644 index 0000000..fc39e45 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs @@ -0,0 +1,16 @@ +namespace WorldDirect.CoAP.Server.Extensions.Configuration; + +using System.Net; + +public class ListenOption +{ + + public ListenOption(EndPoint endpoint, EndpointConfig endpointConfig) + { + this.Endpoint = endpoint; + this.EndpointConfig = endpointConfig; + } + + public EndPoint Endpoint { get; set; } + public EndpointConfig EndpointConfig { get; set; } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs b/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs new file mode 100644 index 0000000..e2d3320 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs @@ -0,0 +1,11 @@ +namespace WorldDirect.CoAP.Server.Extensions; + +using System.Security.Cryptography.X509Certificates; + +public static class CryptographyExtensions +{ + public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(this X509Certificate2 cert) + { + return new Org.BouncyCastle.X509.X509Certificate(cert.Export(X509ContentType.Cert)); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs b/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs new file mode 100644 index 0000000..f8b2375 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs @@ -0,0 +1,17 @@ +namespace WorldDirect.CoAP.Server.Extensions; + +using DTLS; + +internal class DTLSFactory : IDTLSFactory +{ + private readonly DTLSServerBuilder builder; + + public DTLSFactory(DTLSServerBuilder builder) + { + this.builder = builder; + } + public DTLSServer CreateServer() + { + return this.builder.Build(); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs b/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs new file mode 100644 index 0000000..8a622d2 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs @@ -0,0 +1,267 @@ +namespace WorldDirect.CoAP.Server.Extensions; + +using System.Security.Cryptography.X509Certificates; +using Configuration; +using DTLS; +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.Impl.BC; + +public class DTLSServerBuilder +{ + // TODO: Check if certificate usage is allowed for server auth when loaded from files + // TODO: Check if CA is allowed to be used for (KeyCertSign) when loaded from file + private readonly BcTlsCrypto crypto; + private readonly DTLSServerConfig config; + public DTLSServerBuilder() + { + this.crypto = new BcTlsCrypto(new SecureRandom()); + this.config = new DTLSServerConfig(); + } + + /// + /// Loads the servers certificate based on the configuration settings. + /// + /// The settings to identify the certificate. + /// + /// + public DTLSServerBuilder SetCertificate(CertificateConfig config) + { + if (config.IsFromStore) + { + var ecdasCert = this.LoadCertAndKeyFromStore(config); + this.config.EcCertificate = ecdasCert; + } + else if (config.IsFile) + { + try + { + // *.pem and *.key file + if (!string.IsNullOrEmpty(config.Path) && !string.IsNullOrEmpty(config.KeyPath)) + { + var ecdsaCert = this.LoadCertAndKeyFromFiles(config); + this.config.EcCertificate = ecdsaCert; + } + // pfx file + else if (!string.IsNullOrEmpty(config.Path)) + { + var ecdsaCert = this.LoadCertAndKeyFromPfxFile(config); + this.config.EcCertificate = ecdsaCert; + } + else + { + throw new InvalidOperationException("Invalid configuration for certificate"); + } + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load certificate file {config.Path}", e); + } + } + else + { + throw new InvalidOperationException($"Invalid configuration for certificate"); + } + this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8); + this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); + + return this; + } + + public DTLSServerBuilder SetCA(CertificateConfig config) + { + if (config.IsFromStore) + { + var cert = this.LoadCAFromStore(config); + this.config.CA = cert; + } + else if (config.IsFile) + { + try + { + // pfx file, password can be empty + if (!string.IsNullOrEmpty(config.Path) && config.Password != null) + { + throw new NotImplementedException(""); + } + // pem file + else if (!string.IsNullOrEmpty(config.Path)) + { + var cert = this.LoadCertFromFile(config.Path!); + this.config.CA = cert; + } + else + { + throw new InvalidOperationException("Invalid file configuration for CA certificate."); + } + } + catch (IOException e) + { + throw new InvalidOperationException($"Failed to load CA certificate file {config.Path}", e); + } + } + else + { + throw new InvalidOperationException($"Could not identify where to search for CA certificate."); + } + return this; + } + + public DTLSServerBuilder SetHandshakeTimeout(TimeSpan timeout) + { + this.config.HandshakeTimeout = timeout; + return this; + } + + + public DTLSServer Build() + { + return new DTLSServer(this.crypto, this.config); + } + + 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(CertificateConfig 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)); + + object keyObj = pemReader.ReadObject(); + + if (keyObj.GetType() != typeof(ECPrivateKeyParameters)) + { + throw new InvalidOperationException($"{config.KeyPath} does not store a EC key. Currently only ECDSA is supported"); + } + + var key = keyObj 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, key!); + return ecCert; + } + + private EcServerCertificate LoadCertAndKeyFromPfxFile(CertificateConfig 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(CertificateConfig 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(CertificateConfig 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.Server.Extensions/InMemoryPasswordFinder.cs b/WorldDirect.CoAP.Server.Extensions/InMemoryPasswordFinder.cs new file mode 100644 index 0000000..c9ac877 --- /dev/null +++ b/WorldDirect.CoAP.Server.Extensions/InMemoryPasswordFinder.cs @@ -0,0 +1,16 @@ +namespace WorldDirect.CoAP.Server.Extensions; + +using Org.BouncyCastle.OpenSsl; + +internal class InMemoryPasswordFinder : IPasswordFinder +{ + private readonly string password; + public InMemoryPasswordFinder(string password) + { + this.password = password; + } + public char[] GetPassword() + { + return this.password.ToCharArray(); + } +} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/README.md b/WorldDirect.CoAP.Server.Extensions/README.md index 5feeb24..c0c6db6 100644 --- a/WorldDirect.CoAP.Server.Extensions/README.md +++ b/WorldDirect.CoAP.Server.Extensions/README.md @@ -7,7 +7,7 @@ # Examples ## CoAP Server Config Unsecure -``` +``` json "Coap": { "Endpoints": { "CoAP": { @@ -18,7 +18,7 @@ ``` ## CoAPS Server Config with Certificate from pfx file without client authentication -``` +``` json "Coap": { "Endpoints": { "CoAPS": { @@ -26,14 +26,15 @@ "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": { @@ -42,14 +43,15 @@ "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": { @@ -59,8 +61,28 @@ "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.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index 8e598b1..069a39c 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -2,15 +2,12 @@ { using System; using System.Collections.Generic; - using System.Linq; using System.Net; using System.Security.Cryptography; - using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Channel; using Configuration; - using DTLS; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -18,447 +15,18 @@ using Org.BouncyCastle.Asn1.Cmp; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; - using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Ocsp; - using Org.BouncyCastle.OpenSsl; - using Org.BouncyCastle.Pkcs; - using Org.BouncyCastle.Security; - using Org.BouncyCastle.Tls; - using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.X509; - 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; - } - } - - internal class InMemoryPasswordFinder : IPasswordFinder - { - private readonly string password; - public InMemoryPasswordFinder(string password) - { - this.password = password; - } - public char[] GetPassword() - { - return this.password.ToCharArray(); - } - } - - public class DTLSServerBuilder - { - // TODO: Check if certificate usage is allowed for server auth when loaded from files - // TODO: Check if CA is allowed to be used for (KeyCertSign) when loaded from file - private readonly BcTlsCrypto crypto; - private readonly DTLSServerConfig config; - public DTLSServerBuilder() - { - this.crypto = new BcTlsCrypto(new SecureRandom()); - this.config = new DTLSServerConfig(); - } - - public DTLSServerBuilder AddCertificate(CertificateConfig config) - { - // todo check if multiple certificates are valid for same host - // see DTLSServer FindCertificate Function for more information - if (config.IsFromStore) - { - var ecdasCert = this.LoadCertAndKeyFromStore(config); - this.config.EcCertificates.Add(ecdasCert); - } - else if (config.IsFile) - { - try - { - // *.pem and *.key file - if (!string.IsNullOrEmpty(config.Path) && !string.IsNullOrEmpty(config.KeyPath)) - { - var ecdsaCert = this.LoadCertAndKeyFromFiles(config); - this.config.EcCertificates.Add(ecdsaCert); - } - // pfx file - else if (!string.IsNullOrEmpty(config.Path)) - { - var ecdsaCert = this.LoadCertAndKeyFromPfxFile(config); - this.config.EcCertificates.Add(ecdsaCert); - } - else - { - throw new InvalidOperationException("Invalid configuration for certificate"); - } - } - catch (IOException e) - { - throw new InvalidOperationException($"Failed to load certificate file {config.Path}", e); - } - } - else - { - throw new InvalidOperationException($"Invalid configuration for certificate"); - } - this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8); - this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); - - return this; - } - - public DTLSServerBuilder AddCA(CertificateConfig config) - { - if (config.IsFromStore) - { - var cert = this.LoadCAFromStore(config); - this.config.CAs.Add(cert); - } - else if (config.IsFile) - { - try - { - // pem file - if (!string.IsNullOrEmpty(config.Path)) - { - var cert = this.LoadCertFromFile(config.Path!); - this.config.CAs.Add(cert); - } - else - { - throw new InvalidOperationException("Invalid configuration for CA certificate"); - } - } - catch (IOException e) - { - throw new InvalidOperationException($"Failed to load CA certificate file {config.Path}", e); - } - } - else - { - throw new InvalidOperationException($"Invalid configuration for CA certificate"); - } - return this; - } - - public DTLSServerBuilder SetHandshakeTimeout(TimeSpan timeout) - { - this.config.HandshakeTimeout = timeout; - return this; - } - - - public DTLSServer Build() - { - return new DTLSServer(this.crypto, this.config); - } - - 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(CertificateConfig 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)); - - object keyObj = pemReader.ReadObject(); - - if (keyObj.GetType() != typeof(ECPrivateKeyParameters)) - { - throw new InvalidOperationException($"{config.KeyPath} does not store a EC key. Currently only ECDSA is supported"); - } - - var key = keyObj 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, key!); - return ecCert; - } - - private EcServerCertificate LoadCertAndKeyFromPfxFile(CertificateConfig 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(CertificateConfig 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(CertificateConfig config) - { - var cert = CertificateManager.LoadCAFromStore(config.Store!, config.Location!, config.Subject!, config.AllowInvalid); - return this.ToBcCertificate(cert); - } - - private Org.BouncyCastle.X509.X509Certificate ToBcCertificate(X509Certificate2 cert) - { - return new Org.BouncyCastle.X509.X509Certificate(cert.Export(X509ContentType.Cert)); - } - - 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; - } - } - - internal class DTLSFactory : IDTLSFactory - { - private readonly DTLSServerBuilder builder; - - public DTLSFactory(DTLSServerBuilder builder) - { - this.builder = builder; - } - public DTLSServer CreateServer() - { - return this.builder.Build(); - } - } - public static class ServiceProviderExtensions { /// - /// Requires an in the service provider. + /// Configures a based on the provided configuration. /// + /// + /// If DTLS is used for for encryption a must be provided in the service provider. + /// /// /// /// @@ -487,15 +55,12 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura else { var dtlsServerBuilder = new DTLSServerBuilder() - .AddCertificate(listenEndpoint.EndpointConfig.CertificateConfig) + .SetCertificate(listenEndpoint.EndpointConfig.CertificateConfig) .SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); - if (listenEndpoint.EndpointConfig.ClientCA != null && listenEndpoint.EndpointConfig.ClientCA.Length > 0) + if (listenEndpoint.EndpointConfig.ClientCA != null) { - foreach (var ca in listenEndpoint.EndpointConfig.ClientCA) - { - dtlsServerBuilder.AddCA(ca); - } + dtlsServerBuilder.SetCA(listenEndpoint.EndpointConfig.ClientCA); } var channel = new UDPChannel(listenEndpoint.Endpoint); diff --git a/WorldDirect.CoAP/Net/CoAPEndPoint.cs b/WorldDirect.CoAP/Net/CoAPEndPoint.cs index 7f1e017..7a12c81 100644 --- a/WorldDirect.CoAP/Net/CoAPEndPoint.cs +++ b/WorldDirect.CoAP/Net/CoAPEndPoint.cs @@ -35,6 +35,8 @@ public partial class CoAPEndPoint : IEndPoint, IOutbox private System.Net.EndPoint _localEP; private IExecutor _executor; + public string Scheme => CoapConstants.UriScheme; + /// public event EventHandler> SendingRequest; /// 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/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/Resources/CoapExchange.cs b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs index 6cdbf6a..8d47c9a 100644 --- a/WorldDirect.CoAP/Server/Resources/CoapExchange.cs +++ b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs @@ -39,6 +39,11 @@ internal CoapExchange(Exchange exchange, Resource resource) _resource = resource; } + public T Get(Object key) + { + return this._exchange.Get(key); + } + /// /// Gets the request. /// From d9c2ded3fc94661e5fcd16fd2e04cd5dd6ba332f Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 11 Jul 2023 17:10:33 +0200 Subject: [PATCH 04/27] feature complete --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 9 ++-- WorldDirect.CoAP.DTLS/DTLSServer.cs | 43 ++++++++++++++++--- WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs | 2 +- WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 10 ++++- WorldDirect.CoAP.DTLS/DTLSSession.cs | 16 ++++++- .../Configuration/ConfigurationReaderSpecs.cs | 2 +- .../Configuration/CoAPServerOptions.cs | 1 + .../Configuration/CoAPServerOptionsLoader.cs | 17 ++++++-- .../Configuration/ConfigurationReader.cs | 11 +++-- .../Configuration/EndpointConfig.cs | 3 +- .../Configuration/ListenOption.cs | 3 +- .../DTLSServerBuilder.cs | 23 +++++++--- .../ServiceProviderExtensions.cs | 35 ++++++++++++--- 13 files changed, 140 insertions(+), 35 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 1858d26..6c731d7 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -59,9 +59,9 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox /// Instantiates a new endpoint with the /// specified channel and configuration. /// - public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory) + public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, ICoapConfig config) { - _config = CoapConfig.Default; + _config = config; _matcher = new Matcher(this._config); _coapStack = new CoapStack(this._config); UDPChannel channel = new UDPChannel(new IPEndPoint(IPAddress.Any, 5684)); @@ -70,11 +70,12 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory) channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; this.channel = new DTLSChannel(channel, cache, factory); this.channel.DtlsDataReceived += Channel_DataReceived; + this.log = LogManager.GetLogger(); } - public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel) + public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel, ICoapConfig config) { - _config = CoapConfig.Default; + _config = config; _matcher = new Matcher(this._config); _coapStack = new CoapStack(this._config); this.channel = new DTLSChannel(channel, cache, factory); diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index 45ad96f..6d40569 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -33,7 +33,19 @@ public DTLSServer(BcTlsCrypto crypto, DTLSServerConfig config) : base(crypto) /// /// Gets the certificate of the connected client. /// - public TlsCertificate? PeerCertificate => this.m_context.SecurityParameters.PeerCertificate.IsEmpty ? null : this.m_context.SecurityParameters.PeerCertificate.GetCertificateAt(0); + 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); + } + } + + public byte[] PskIdentity { get; private set; } = Array.Empty(); /// /// Get the timeout of handshake. @@ -62,6 +74,24 @@ 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; + } + } + + public override TlsPskIdentityManager GetPskIdentityManager() + { + if (this.config.PskManager == null) + { + return null; + } + return this.config.PskManager; + } + /// /// Get the certificate request send to the client. /// @@ -69,7 +99,7 @@ protected override int[] GetSupportedCipherSuites() public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() { // if no CAs are registered, we wont need a certificate for authentication. - if (this.config.CA == null) + if (!this.config.CAs.Any()) { return null; } @@ -80,7 +110,8 @@ public override Org.BouncyCastle.Tls.CertificateRequest GetCertificateRequest() serverSigAlgs = serverSigAlgs.Where(s => s.Signature == SignatureAlgorithm.ecdsa).ToList(); // send back a list of supported CAs - var authorities = new List() {this.config.CA.SubjectDN}; + + var authorities = this.config.CAs.Select(c => c.SubjectDN).ToList(); short[] certificateTypes = new short[] { ClientCertificateType.ecdsa_sign, }; @@ -97,6 +128,8 @@ public override TlsCredentials GetCredentials() int keyExchangeAlgorithm = m_context.SecurityParameters.KeyExchangeAlgorithm; switch (keyExchangeAlgorithm) { + case KeyExchangeAlgorithm.PSK: + return null; case KeyExchangeAlgorithm.ECDHE_ECDSA: return GetECDsaSignerCredentials(); default: @@ -117,9 +150,9 @@ public override void NotifyClientCertificate(Certificate clientCertificate) var chain = clientCertificate.GetCertificateList()!; var chainAsCertificate = chain.Select(c => new X509Certificate(c.GetEncoded())).ToArray(); - var trustAnchor = new TrustAnchor(this.config.CA, null); + var trustAnchors = this.config.CAs.Select(c => new TrustAnchor(c, null)).ToList(); - var parameters = new PkixParameters(new SortedSet() { trustAnchor }); + var parameters = new PkixParameters(new SortedSet(trustAnchors)); parameters.IsRevocationEnabled = false; var path = new PkixCertPath(chainAsCertificate); var validator = new PkixCertPathValidator(); diff --git a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs index 1471034..e22bf9a 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs @@ -109,7 +109,7 @@ public DTLSServerBuilder WithTrustedRoot(string filename) throw new InvalidOperationException($"Could not read certificate from {filename}"); } - this.config.CA = cert; + this.config.CAs.Add(cert); return this; } diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs index 0580d88..b64cfb9 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -1,5 +1,8 @@ namespace WorldDirect.CoAP.DTLS; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.X509; + /// /// Represents the configuration of the . /// @@ -13,7 +16,7 @@ public class DTLSServerConfig /// /// Gets or sets the CA to authorize the connecting clients. /// - public Org.BouncyCastle.X509.X509Certificate? CA { get; set; } + public List CAs { get; set; } = new List(); /// /// Gets or sets the available cipher suites. @@ -27,4 +30,9 @@ public class DTLSServerConfig /// 0 means no timeout /// public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; + + /// + /// Gets or sets the provider for psk keys. + /// + public TlsPskIdentityManager? PskManager { get; set; } } diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 49bd472..036dbf3 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -1,9 +1,13 @@ namespace WorldDirect.CoAP.DTLS; +using System.Data; using System.Net; using System.Security.Cryptography.X509Certificates; +using System.Text; using Channel; using Org.BouncyCastle.Tls; +using WorldDirect.CoAP.Log; +using WorldDirect.CoAP.Net; internal class DTLSSession { @@ -15,6 +19,7 @@ internal class DTLSSession private DtlsTransport? dtlsTransport; private Task? HandshakeTask; private bool HandshakeFailed = false; + private readonly ILogger logger; public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, CancellationTokenSource cts, DTLSSessionConfig config) { @@ -23,6 +28,7 @@ public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, Cancel this.transport = new UdpTransport(sender, remote, config.MaxPacketLength); this.protocol = new DtlsServerProtocol(); this.dtlsServer = server; + this.logger = LogManager.GetLogger(typeof(DTLSSession)); } public EndPoint Remote => this.transport.Remote; @@ -93,6 +99,10 @@ public void Enqueue(ReadOnlySpan payload) var peerCert = new X509Certificate(this.dtlsServer.PeerCertificate.GetEncoded()); this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, peerCert)); } + else if (this.dtlsServer.PskIdentity.Any()) + { + this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, Encoding.ASCII.GetString(this.dtlsServer.PskIdentity))); + } } throw new NotImplementedException($"PSK or unauthenticated communication is not implemented"); } @@ -110,7 +120,11 @@ private Task HandleSession() } catch (TlsTimeoutException e) { - // todo logging handshake timed out + this.HandshakeFailed = true; + } + catch (TlsFatalAlert e) + { + // todo logging this.HandshakeFailed = true; } catch (Exception e) diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs index 78501f3..8ca4b25 100644 --- a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs +++ b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs @@ -64,7 +64,7 @@ public void ReadsTwoClientCAs() { var endpoints = this.reader.Endpoints; var endpoint = endpoints.Single(); - endpoint.ClientCA.Should().HaveCount(2); + endpoint.ClientCAs.Should().HaveCount(2); } [Fact] diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs index 894adba..e76599e 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs @@ -12,5 +12,6 @@ internal CoAPServerOptions(IEnumerable listenOptions) public IEnumerable ListenOptions { get; } + public int MaxMessageSize { get; set; } = 1024; } } diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs index ba34394..49c8f8d 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs @@ -30,10 +30,21 @@ private CoAPServerOptions Build() } else { - listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Any, address.Port), endpoint)); + var ipAddress = IPAddress.Any; + if(IPAddress.TryParse(address.Host, out var parsedAddress)) + { + ipAddress = parsedAddress; + } + listenOptions.Add(new ListenOption(new IPEndPoint(ipAddress, address.Port), endpoint)); } } - return new CoAPServerOptions(listenOptions); + var options = new CoAPServerOptions(listenOptions); + if(reader.MaxMessageSize.HasValue) + { + options.MaxMessageSize = reader.MaxMessageSize.Value; + } + + return options; } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs index a61c870..74e230a 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs @@ -16,6 +16,7 @@ public class ConfigurationReader private const string CertificateKey = "Certificate"; private const string ClientCAKey = "ClientCA"; private const string HandshakeTimeoutKey = "HandshakeTimeout"; + private const string MaxMessageSizeKey = "MaxMessageSize"; private readonly IConfiguration config; public ConfigurationReader(IConfiguration config) @@ -25,6 +26,8 @@ public ConfigurationReader(IConfiguration config) public IEnumerable Endpoints => this.ReadEndpoints(); + public int? MaxMessageSize => this.config.GetSection(MaxMessageSizeKey).Exists() ? this.config.GetSection(MaxMessageSizeKey).Get() : null; + private IEnumerable ReadEndpoints() { var endpoints = new List(); @@ -44,15 +47,15 @@ private IEnumerable ReadEndpoints() { certificateConfig = new CertificateConfig(endpointCfg.GetSection(CertificateKey)); } - CertificateConfig? clientCAConfig = null; + IEnumerable? clientCAConfig = null; if (endpointCfg.GetSection(ClientCAKey).GetChildren().Any()) { - clientCAConfig = new CertificateConfig(endpointCfg.GetSection(ClientCAKey)); + clientCAConfig = endpointCfg.GetSection(ClientCAKey).GetChildren().Select(c => new CertificateConfig(c)); } var endpoint = new EndpointConfig(endpointCfg.Key, url) { CertificateConfig = certificateConfig, - ClientCA = clientCAConfig, + ClientCAs = clientCAConfig != null ? clientCAConfig.ToList() : new List(), HandshakeTimeout = endpointCfg.GetSection(HandshakeTimeoutKey).Get(), }; @@ -61,4 +64,4 @@ private IEnumerable ReadEndpoints() return endpoints; } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs index 6f4d50b..c86c991 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs @@ -16,12 +16,13 @@ public EndpointConfig(string name, string url) { this.Name = name; this.Url = url; + this.ClientCAs = new List(); } public string Name { get; set; } public string Url { get; set; } public CertificateConfig? CertificateConfig { get; set; } - public CertificateConfig? ClientCA { get; set; } + public List ClientCAs { get; set; } public TimeSpan HandshakeTimeout { get; set; } } } diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs index fc39e45..43cfc9d 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs +++ b/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs @@ -10,7 +10,6 @@ public ListenOption(EndPoint endpoint, EndpointConfig endpointConfig) this.Endpoint = endpoint; this.EndpointConfig = endpointConfig; } - public EndPoint Endpoint { get; set; } public EndpointConfig EndpointConfig { get; set; } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs b/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs index 8a622d2..b2cc429 100644 --- a/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs +++ b/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs @@ -71,12 +71,12 @@ public DTLSServerBuilder SetCertificate(CertificateConfig config) return this; } - public DTLSServerBuilder SetCA(CertificateConfig config) + public DTLSServerBuilder AddCA(CertificateConfig config) { if (config.IsFromStore) { var cert = this.LoadCAFromStore(config); - this.config.CA = cert; + this.config.CAs.Add(cert); } else if (config.IsFile) { @@ -91,7 +91,7 @@ public DTLSServerBuilder SetCA(CertificateConfig config) else if (!string.IsNullOrEmpty(config.Path)) { var cert = this.LoadCertFromFile(config.Path!); - this.config.CA = cert; + this.config.CAs.Add(cert); } else { @@ -116,6 +116,14 @@ public DTLSServerBuilder SetHandshakeTimeout(TimeSpan timeout) return this; } + public DTLSServerBuilder SetPskManager(TlsPskIdentityManager manager) + { + this.config.PskManager = manager; + this.config.CipherSuites.Add(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8); + this.config.CipherSuites.Add(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256); + return this; + } + public DTLSServer Build() { @@ -144,21 +152,22 @@ private EcServerCertificate LoadCertAndKeyFromFiles(CertificateConfig config) using var textReader = new StreamReader(reader); PemReader pemReader = new PemReader(textReader, new InMemoryPasswordFinder(password)); - object keyObj = pemReader.ReadObject(); + var keyObj = pemReader.ReadPemObject(); + var key = PrivateKeyFactory.CreateKey(keyObj.Content); - if (keyObj.GetType() != typeof(ECPrivateKeyParameters)) + if (key.GetType() != typeof(ECPrivateKeyParameters)) { throw new InvalidOperationException($"{config.KeyPath} does not store a EC key. Currently only ECDSA is supported"); } - var key = keyObj as ECPrivateKeyParameters; + 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, key!); + var ecCert = new EcServerCertificate(ecdsaCertificate, caPrivateKey!); return ecCert; } diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index 069a39c..907b2e0 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Net; + using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -16,8 +17,17 @@ using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Ocsp; + using Org.BouncyCastle.Tls; using Org.BouncyCastle.X509; + /// + /// A helper function to determinate how PSKs are loaded and mapped to a CoAPS endpoint. + /// + /// The service provider to load needed services from. + /// The name of the endpoint configuration. + /// The psk store. + public delegate TlsPskIdentityManager? PskIdentityManagerResolver(IServiceProvider serviceProvider, string key); + public static class ServiceProviderExtensions { @@ -26,6 +36,7 @@ public static class ServiceProviderExtensions /// /// /// If DTLS is used for for encryption a must be provided in the service provider. + /// If PSKs should be used must be added to the ServiceCollection. /// /// /// @@ -49,7 +60,7 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura { if (listenEndpoint!.EndpointConfig.CertificateConfig == null) { - // unsecure + // insecure server.AddEndPoint(listenEndpoint.Endpoint as IPEndPoint); } else @@ -57,19 +68,33 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura var dtlsServerBuilder = new DTLSServerBuilder() .SetCertificate(listenEndpoint.EndpointConfig.CertificateConfig) .SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); + var resolver = serviceProvider.GetService(); + if (resolver != null) + { + var pskManager = resolver(serviceProvider, listenEndpoint.EndpointConfig.Name); + if (pskManager != null) + { + dtlsServerBuilder.SetPskManager(pskManager); + } + } - if (listenEndpoint.EndpointConfig.ClientCA != null) + foreach (var ca in listenEndpoint.EndpointConfig.ClientCAs) { - dtlsServerBuilder.SetCA(listenEndpoint.EndpointConfig.ClientCA); + dtlsServerBuilder.AddCA(ca); } var channel = new UDPChannel(listenEndpoint.Endpoint); - var config = CoapConfig.Default; + var config = (CoapConfig)CoapConfig.Default; + config.MaxMessageSize = options.MaxMessageSize; + if(config.MaxMessageSize <= config.DefaultBlockSize) + { + config.DefaultBlockSize = config.MaxMessageSize / 2; + } channel.ReceiveBufferSize = config.ChannelReceiveBufferSize; channel.SendBufferSize = config.ChannelSendBufferSize; channel.ReceivePacketSize = config.ChannelReceivePacketSize; - var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), new DTLSFactory(dtlsServerBuilder), channel); + var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), new DTLSFactory(dtlsServerBuilder), channel, config); server.AddEndPoint(ep); } From 3358435583cb90ffc1bdcad8bf69033d1ba0ae63 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Thu, 13 Jul 2023 08:50:21 +0200 Subject: [PATCH 05/27] add microsoft logging --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 136 +--------- WorldDirect.CoAP.DTLS/DTLSSession.cs | 35 ++- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 15 +- .../ServiceProviderExtensions.cs | 2 + WorldDirect.CoAP/Channel/UDPChannel.NET40.cs | 3 +- WorldDirect.CoAP/Channel/UDPChannel.cs | 9 +- WorldDirect.CoAP/CoapClient.cs | 6 +- WorldDirect.CoAP/Codec/DatagramWriter.cs | 6 +- .../Deduplication/DeduplicatorFactory.cs | 6 +- .../Deduplication/SweepDeduplicator.cs | 9 +- .../EndPoint/Resources/Resource.cs | 15 +- WorldDirect.CoAP/LinkAttribute.cs | 6 +- WorldDirect.CoAP/LinkFormat.cs | 6 +- WorldDirect.CoAP/Log/CommonLoggingManager.cs | 238 ------------------ WorldDirect.CoAP/Log/ConsoleLogManager.cs | 30 --- WorldDirect.CoAP/Log/ILogManager.cs | 30 --- WorldDirect.CoAP/Log/ILogger.cs | 82 ------ WorldDirect.CoAP/Log/LogManager.cs | 79 +----- WorldDirect.CoAP/Log/NopLogManager.cs | 122 --------- WorldDirect.CoAP/Log/TextWriterLogger.cs | 178 ------------- WorldDirect.CoAP/Net/CoAPEndPoint.cs | 28 +-- WorldDirect.CoAP/Net/Matcher.cs | 63 ++--- WorldDirect.CoAP/Observe/ObserveRelation.cs | 6 +- WorldDirect.CoAP/Server/CoapServer.cs | 12 +- WorldDirect.CoAP/Server/Resources/Resource.cs | 9 +- .../Server/ServerMessageDeliverer.cs | 6 +- WorldDirect.CoAP/Stack/BlockwiseLayer.cs | 93 +++---- WorldDirect.CoAP/Stack/ObserveLayer.cs | 36 +-- WorldDirect.CoAP/Stack/ReliabilityLayer.cs | 57 ++--- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 30 files changed, 201 insertions(+), 1124 deletions(-) delete mode 100644 WorldDirect.CoAP/Log/CommonLoggingManager.cs delete mode 100644 WorldDirect.CoAP/Log/ConsoleLogManager.cs delete mode 100644 WorldDirect.CoAP/Log/ILogManager.cs delete mode 100644 WorldDirect.CoAP/Log/ILogger.cs delete mode 100644 WorldDirect.CoAP/Log/NopLogManager.cs delete mode 100644 WorldDirect.CoAP/Log/TextWriterLogger.cs diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 6c731d7..4bda737 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -20,6 +20,7 @@ namespace WorldDirect.CoAP.Net using DTLS; using Log; using Microsoft.Extensions.Caching.Memory; + using Microsoft.Extensions.Logging; using Org.BouncyCastle.Tls; using Stack; using Threading; @@ -29,7 +30,6 @@ namespace WorldDirect.CoAP.Net /// public partial class CoAPSEndpoint : IEndPoint, IOutbox { - static readonly ILogger log = LogManager.GetLogger(typeof(CoAPSEndpoint)); readonly ICoapConfig _config; readonly CoapStack _coapStack; @@ -38,6 +38,7 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox private Int32 _running; private IExecutor _executor; private DTLSChannel channel; + private ILogger log = LogManager.GetLogger(); /// public string Scheme => CoapConstants.SecureUriScheme; @@ -70,7 +71,6 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, ICoapConfig confi channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; this.channel = new DTLSChannel(channel, cache, factory); this.channel.DtlsDataReceived += Channel_DataReceived; - this.log = LogManager.GetLogger(); } public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel, ICoapConfig config) @@ -142,13 +142,11 @@ public void Start() } catch { - if (log.IsWarnEnabled) - log.Warn("Cannot start secure endpoint at " + this.channel.LocalEndPoint); + log.LogWarning("Cannot start secure endpoint at " + this.channel.LocalEndPoint); Stop(); throw; } - if (log.IsDebugEnabled) - log.Debug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); + log.LogDebug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); } /// @@ -156,8 +154,8 @@ public void Stop() { if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) return; - if (log.IsDebugEnabled) - log.Debug("Stopping secure endpoint bound to " + this.LocalEndPoint); + + log.LogDebug("Stopping secure endpoint bound to " + this.LocalEndPoint); this.channel.Stop(); _matcher.Stop(); _matcher.Clear(); @@ -213,8 +211,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) { 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 { @@ -226,8 +223,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) Fire(SendingEmptyMessage, rst); this.channel.Send(Serialize(rst), e.EndPoint); - 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; } @@ -265,8 +261,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs 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); } } @@ -283,8 +278,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs 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 @@ -298,118 +292,12 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs 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); } } - /*private void ReceiveData(DTLSDecryptedDataReceivedEventArgs e) - { - IMessageDecoder decoder = Spec.NewMessageDecoder(e.Payload); - if (decoder.IsRequest) - { - Request request; - try - { - request = decoder.DecodeRequest(); - } - catch (Exception) - { - if (decoder.IsReply) - { - if (log.IsWarnEnabled) - log.Warn("Message format error caused by " + e.Remote.Remote); - } - else - { - // manually build RST from raw information - EmptyMessage rst = new EmptyMessage(MessageType.RST); - rst.Destination = e.Remote.Remote; - rst.ID = decoder.ID; - - Fire(SendingEmptyMessage, rst); - - _dtlsStack.SendTo(Serialize(rst), e.Remote.Remote); - - if (log.IsWarnEnabled) - log.Warn("Message format error caused by " + e.Remote.Remote + " and reseted."); - } - return; - } - - request.Source = e.Remote.Remote; - - Fire(ReceivingRequest, request); - - if (!request.IsCancelled) - { - Exchange exchange = _matcher.ReceiveRequest(request); - if (exchange != null) - { - exchange.EndPoint = this; - exchange.Set(nameof(DTLSClient), e.Remote); - _coapStack.ReceiveRequest(exchange, request); - } - } - } - else if (decoder.IsResponse) - { - Response response = decoder.DecodeResponse(); - response.Source = e.Remote.Remote; - - Fire(ReceivingResponse, response); - - if (!response.IsCancelled) - { - Exchange exchange = _matcher.ReceiveResponse(response); - if (exchange != null) - { - response.RTT = (DateTime.Now - exchange.Timestamp).TotalMilliseconds; - exchange.EndPoint = this; - _coapStack.ReceiveResponse(exchange, response); - } - else if (response.Type != MessageType.ACK) - { - if (log.IsDebugEnabled) - log.Debug("Rejecting unmatchable response from " + e.Remote.Remote); - Reject(response); - } - } - } - else if (decoder.IsEmpty) - { - EmptyMessage message = decoder.DecodeEmptyMessage(); - message.Source = e.Remote.Remote; - - Fire(ReceivingEmptyMessage, message); - - if (!message.IsCancelled) - { - // CoAP Ping - if (message.Type == MessageType.CON || message.Type == MessageType.NON) - { - if (log.IsDebugEnabled) - log.Debug("Responding to ping by " + e.Remote.Remote); - Reject(message); - } - else - { - Exchange exchange = _matcher.ReceiveEmptyMessage(message); - if (exchange != null) - { - exchange.EndPoint = this; - _coapStack.ReceiveEmptyMessage(exchange, message); - } - } - } - } - else if (log.IsDebugEnabled) - { - log.Debug("Silently ignoring non-CoAP message from " + e.Remote.Remote); - } - }*/ - private void Reject(Message message) { EmptyMessage rst = EmptyMessage.NewRST(message); diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 036dbf3..a44fd20 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -5,6 +5,7 @@ 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; @@ -19,7 +20,7 @@ internal class DTLSSession private DtlsTransport? dtlsTransport; private Task? HandshakeTask; private bool HandshakeFailed = false; - private readonly ILogger logger; + private readonly ILogger logger; public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, CancellationTokenSource cts, DTLSSessionConfig config) { @@ -28,7 +29,7 @@ public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, Cancel this.transport = new UdpTransport(sender, remote, config.MaxPacketLength); this.protocol = new DtlsServerProtocol(); this.dtlsServer = server; - this.logger = LogManager.GetLogger(typeof(DTLSSession)); + this.logger = LogManager.GetLogger(); } public EndPoint Remote => this.transport.Remote; @@ -56,8 +57,10 @@ public void Cancel() /// public void Start() { + var th = new Thread(async () => await this.HandleSession()); + th.Start(); // perform handshake asynchronously, would be blocking otherwise - Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); + //Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); } /// @@ -85,12 +88,17 @@ public void Enqueue(ReadOnlySpan payload) if (dtlsTransport != null) { var rxBuffer = new byte[this.config.MaxPacketLength]; - var length = this.dtlsTransport!.Receive(rxBuffer, 0); - if (length < 0) + var length = 0; + try { - throw new InvalidOperationException("Could not read from dtls"); + length = this.dtlsTransport!.Receive(rxBuffer, 1000); } - else if (length > 0) + catch(Exception ex) + { + this.logger.LogTrace(ex, "Cant receive decrypted dtls packet from {Remote}", this.Remote); + } + + if (length > 0) { if (this.dtlsServer.IsAuthenticated) { @@ -104,7 +112,11 @@ public void Enqueue(ReadOnlySpan payload) this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, Encoding.ASCII.GetString(this.dtlsServer.PskIdentity))); } } - throw new NotImplementedException($"PSK or unauthenticated communication is not implemented"); + else + { + throw new NotImplementedException($"Unauthenticated communication is not implemented"); + } + } } @@ -116,20 +128,21 @@ private Task HandleSession() { // perform handshake this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); - // todo logging + this.logger.LogInformation("{Remote} finished handshake successfully", this.Remote); } catch (TlsTimeoutException e) { this.HandshakeFailed = true; + this.logger.LogError(e, "{Remote} failed handshake because of timeout", this.Remote); } catch (TlsFatalAlert e) { - // todo logging + this.logger.LogError(e, "{Remote} failed handshake", this.Remote); this.HandshakeFailed = true; } catch (Exception e) { - // Todo logging + this.logger.LogError(e, "{Remote} failed handshake", this.Remote); this.HandshakeFailed = true; } finally diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index e6e9551..0872975 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -6,8 +6,10 @@ using Channel; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Internal; + using Microsoft.Extensions.Logging; using Org.BouncyCastle.Asn1.Nist; using Org.BouncyCastle.Asn1.X509; + using WorldDirect.CoAP.Log; /// /// Handles all dtls related traffic. @@ -19,6 +21,7 @@ public class DTLSSessionManager private readonly IDTLSFactory factory; private readonly DTLSSessionConfig config; private readonly CancellationTokenSource cts; + private readonly ILogger log = LogManager.GetLogger(); /// /// Initializes a new instance of the class. @@ -46,10 +49,14 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, IDTLSFactory fa /// The remote endpoint. public void SendTo(ReadOnlySpan packet, EndPoint endPoint) { - if(this.cache.TryGetValue(endPoint, out var session)) + if(this.cache.TryGetValue(endPoint.ToString(), out var session)) { session.Send(packet); } + else + { + this.log.LogWarning("Tried to send data to {Remote} but no session available", endPoint); + } } /// @@ -67,7 +74,7 @@ public void Stop() /// The endpoint who sent the packet. internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { - var session = this.cache.GetOrCreate(endPoint, entry => + var session = this.cache.GetOrCreate(endPoint.ToString(), entry => { entry.SlidingExpiration = config.SessionTimeout; var callback = new PostEvictionCallbackRegistration() @@ -79,6 +86,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token), this.config); s.DataReceived += DecryptedReceived; s.HandshakeFinished += HandshakeFinished; + this.log.LogInformation("Start DTLS connection with {Remote}", endPoint); s.Start(); return s; }); @@ -91,7 +99,7 @@ private void HandshakeFinished(object? sender, HandshakeFinishedEventArgs e) var session = (sender as DTLSSession)!; if (!e.Successful) { - this.cache.Remove(session.Remote); + this.cache.Remove(session.Remote.ToString()); } } @@ -103,6 +111,7 @@ private void DecryptedReceived(object? _, DTLSDataReceivedEventArgs e) private static void OnEviction(object key, object value, EvictionReason reason, object state) { var obj = value as DTLSSession; + LogManager.GetLogger().LogDebug("Session with {Remote} timed out", obj.Remote); obj?.Cancel(); } } diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index 907b2e0..1967c9a 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -19,6 +19,7 @@ using Org.BouncyCastle.Ocsp; using Org.BouncyCastle.Tls; using Org.BouncyCastle.X509; + using WorldDirect.CoAP.Log; /// /// A helper function to determinate how PSKs are loaded and mapped to a CoAPS endpoint. @@ -50,6 +51,7 @@ public static IServiceCollection ConfigureCoAPServer(this IServiceCollection ser private static CoapServer Configure(IServiceProvider serviceProvider, IConfiguration configuration) { + LogManager.Provider = serviceProvider; var server = new CoapServer(); diff --git a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs index dd717ee..d389c60 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.LogDebug(message: $"Sending packet to {ep.Address.MapToIPv4()}:{ep.Port}. Processing package {(completedSynchronous ? "asynchronous" : "synchronous")}"); } if (completedSynchronous) diff --git a/WorldDirect.CoAP/Channel/UDPChannel.cs b/WorldDirect.CoAP/Channel/UDPChannel.cs index 8de9cbe..66eadfb 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. @@ -252,13 +253,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); } } } diff --git a/WorldDirect.CoAP/CoapClient.cs b/WorldDirect.CoAP/CoapClient.cs index 32f7913..4d25870 100644 --- a/WorldDirect.CoAP/CoapClient.cs +++ b/WorldDirect.CoAP/CoapClient.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Log; + using Microsoft.Extensions.Logging; using Net; /// @@ -15,7 +16,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; @@ -587,8 +588,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/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..3c18a09 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..dce0b56 100644 --- a/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs +++ b/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs @@ -16,11 +16,12 @@ namespace WorldDirect.CoAP.Deduplication using System.Collections.Generic; 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(); @@ -34,8 +35,7 @@ public SweepDeduplicator(ICoapConfig config) private void Sweep(object state) { - if (log.IsDebugEnabled) - log.Debug("Start Mark-And-Sweep with " + _incommingMessages.Count + " entries"); + log.LogDebug("Start Mark-And-Sweep with " + _incommingMessages.Count + " entries"); DateTime oldestAllowed = DateTime.Now.AddMilliseconds(-_config.ExchangeLifetime); List keysToRemove = new List(); @@ -43,8 +43,7 @@ private void Sweep(object state) { if (pair.Value.Timestamp < oldestAllowed) { - if (log.IsDebugEnabled) - log.Debug("Mark-And-Sweep removes " + pair.Key); + log.LogDebug("Mark-And-Sweep removes " + pair.Key); keysToRemove.Add(pair.Key); } } 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..0146ecd 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..b0b710d 100644 --- a/WorldDirect.CoAP/Log/LogManager.cs +++ b/WorldDirect.CoAP/Log/LogManager.cs @@ -12,97 +12,32 @@ namespace WorldDirect.CoAP.Log { using System; + 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/Net/CoAPEndPoint.cs b/WorldDirect.CoAP/Net/CoAPEndPoint.cs index 7a12c81..c62998b 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; @@ -172,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); } /// @@ -186,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(); @@ -247,8 +245,7 @@ private void ReceiveData(DataReceivedEventArgs e) { 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 { @@ -261,8 +258,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; } @@ -299,8 +295,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); } } @@ -317,8 +312,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 @@ -332,9 +326,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/Matcher.cs b/WorldDirect.CoAP/Net/Matcher.cs index ddfdd6f..b83a0f1 100644 --- a/WorldDirect.CoAP/Net/Matcher.cs +++ b/WorldDirect.CoAP/Net/Matcher.cs @@ -16,12 +16,13 @@ namespace WorldDirect.CoAP.Net using System.Collections.Generic; using Deduplication; using Log; + using Microsoft.Extensions.Logging; using Observe; using Util; public class Matcher : IMatcher, IDisposable { - static readonly ILogger log = LogManager.GetLogger(typeof(Matcher)); + static readonly ILogger log = LogManager.GetLogger(); /// /// for all @@ -94,8 +95,7 @@ public void SendRequest(Exchange exchange, Request request) exchange.Completed += OnExchangeCompleted; - if (log.IsDebugEnabled) - log.Debug("Stored open request by " + keyID + ", " + keyToken); + log.LogDebug("Stored open request by " + keyID + ", " + keyToken); _exchangesByID[keyID] = exchange; _exchangesByToken[keyToken] = exchange; @@ -138,19 +138,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); } @@ -215,8 +212,7 @@ public Exchange ReceiveRequest(Request request) } else { - if (log.IsInfoEnabled) - log.Info("Duplicate request: " + request); + log.LogTrace("Duplicate request: {Request}", request); request.Duplicate = true; return previous; } @@ -225,8 +221,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 +229,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 +238,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 +259,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; } @@ -311,23 +302,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.LogInformation("Duplicate response for open exchange: " + response); response.Duplicate = true; } else { keyId = new Exchange.KeyID(exchange.CurrentRequest.ID, null); - if (log.IsDebugEnabled) - log.Debug("Exchange got response: Cleaning up " + keyId); + log.LogDebug("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 +329,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.LogInformation("Ignoring unmatchable piggy-backed response from " + response.Source + ": " + response); } // ignore response return null; @@ -365,15 +351,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; } } @@ -388,8 +372,7 @@ public void 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()) { @@ -414,8 +397,7 @@ private void OnExchangeCompleted(Object sender, EventArgs e) Exchange.KeyID keyID = new Exchange.KeyID(exchange.CurrentRequest.ID, null); Exchange.KeyToken keyToken = new Exchange.KeyToken(exchange.CurrentRequest.Token); - if (log.IsDebugEnabled) - log.Debug("Exchange completed: Cleaning up " + keyToken); + log.LogDebug("Exchange completed: Cleaning up " + keyToken); _exchangesByToken.Remove(keyToken); // in case an empty ACK was lost @@ -439,8 +421,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/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/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/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..86dd69f 100644 --- a/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs +++ b/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs @@ -14,6 +14,7 @@ namespace WorldDirect.CoAP.Server using System; using System.Collections.Generic; using Log; + using Microsoft.Extensions.Logging; using Net; using Observe; using Resources; @@ -26,7 +27,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(); @@ -101,8 +102,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..2c072ec 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.LogDebug("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; @@ -265,9 +253,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 +263,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 +283,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 +297,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 +319,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 +347,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 +372,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 +390,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 +401,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 +420,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 +438,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 +462,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); @@ -664,13 +639,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..1306b5b 100644 --- a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs +++ b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs @@ -14,6 +14,7 @@ namespace WorldDirect.CoAP.Stack using System; using System.Threading; using Log; + using Microsoft.Extensions.Logging; using Net; /// @@ -21,7 +22,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 +46,7 @@ public override void SendRequest(INextLayer nextLayer, Exchange exchange, Reques if (request.Type == MessageType.CON) { - if (log.IsDebugEnabled) - log.Debug("Scheduling retransmission for " + request); + log.LogTrace("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; } @@ -239,8 +231,7 @@ private void PrepareRetransmission(Exchange exchange, Message msg, Action"); - if (_exchange.Origin == Origin.Local) - log.Debug(_exchange.CurrentRequest); - else - log.Debug(_exchange.CurrentResponse); - } } public void Dispose() @@ -334,26 +316,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.LogTrace("Timeout: retransmit message, failed: " + failedCount + ", message: " + _message); _message.FireRetransmitting(); @@ -363,8 +341,7 @@ void timer_Elapsed(object state) } else { - if (log.IsDebugEnabled) - log.Debug("Timeout: retransmission limit reached, exchange failed, message: " + _message); + log.LogTrace("Timeout: retransmission limit reached, exchange failed, message: " + _message); _exchange.TimedOut = true; _message.IsTimedOut = true; _exchange.Remove(TransmissionContextKey); diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index fb4ed6a..1d04f34 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -41,7 +41,7 @@ - + From 8e31ae1e9440b5330718f1c62e674edf09ff0567 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Mon, 17 Jul 2023 15:10:08 +0200 Subject: [PATCH 06/27] fix logging and threading --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 16 ++++++------- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 5 ++++ WorldDirect.CoAP.DTLS/DTLSSession.cs | 23 ++++--------------- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 7 ++---- WorldDirect.CoAP.DTLS/UdpTransport.cs | 2 +- .../WorldDirect.CoAP.DTLS.csproj | 2 +- .../Deduplication/DeduplicatorFactory.cs | 2 +- WorldDirect.CoAP/LinkFormat.cs | 2 +- WorldDirect.CoAP/Log/LogManager.cs | 1 + 9 files changed, 24 insertions(+), 36 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 4bda737..99a6ad3 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -142,11 +142,11 @@ public void Start() } catch { - log.LogWarning("Cannot start secure endpoint at " + this.channel.LocalEndPoint); + log?.LogWarning("Cannot start secure endpoint at " + this.channel.LocalEndPoint); Stop(); throw; } - log.LogDebug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); + log?.LogDebug("Starting secure endpoint bound to " + this.channel.LocalEndPoint); } /// @@ -155,7 +155,7 @@ public void Stop() if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) return; - log.LogDebug("Stopping secure endpoint bound to " + this.LocalEndPoint); + log?.LogDebug("Stopping secure endpoint bound to " + this.LocalEndPoint); this.channel.Stop(); _matcher.Stop(); _matcher.Clear(); @@ -211,7 +211,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) { if (decoder.IsReply) { - log.LogWarning("Message format error caused by " + e.EndPoint); + log?.LogWarning("Message format error caused by " + e.EndPoint); } else { @@ -223,7 +223,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) Fire(SendingEmptyMessage, rst); this.channel.Send(Serialize(rst), e.EndPoint); - log.LogWarning("Message format error caused by " + e.EndPoint + " and reseted."); + log?.LogWarning("Message format error caused by " + e.EndPoint + " and reseted."); } return; } @@ -261,7 +261,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) } else if (response.Type != MessageType.ACK) { - log.LogDebug("Rejecting unmatchable response from " + e.EndPoint); + log?.LogDebug("Rejecting unmatchable response from " + e.EndPoint); Reject(response); } } @@ -278,7 +278,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) // CoAP Ping if (message.Type == MessageType.CON || message.Type == MessageType.NON) { - log.LogDebug("Responding to ping by " + e.EndPoint); + log?.LogDebug("Responding to ping by " + e.EndPoint); Reject(message); } else @@ -294,7 +294,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) } else { - log.LogDebug("Silently ignoring non-CoAP message from " + e.EndPoint); + log?.LogDebug("Silently ignoring non-CoAP message from " + e.EndPoint); } } diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index ecc52cb..52a2d95 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -8,6 +8,8 @@ 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. @@ -16,6 +18,7 @@ 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. @@ -82,11 +85,13 @@ public void Stop() /// public void Send(byte[] data, EndPoint ep) { + this.logger.LogTrace("Sending {Bytes} udp bytes to {Remote}", data.Length, ep); this.sessionManager.SendTo(data, 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/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index a44fd20..81dea34 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -12,19 +12,16 @@ internal class DTLSSession { - private readonly CancellationTokenSource cts; private readonly DTLSSessionConfig config; private readonly UdpTransport transport; private readonly DtlsServerProtocol protocol; private readonly DTLSServer dtlsServer; private DtlsTransport? dtlsTransport; - private Task? HandshakeTask; private bool HandshakeFailed = false; private readonly ILogger logger; - public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, CancellationTokenSource cts, DTLSSessionConfig config) + public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, DTLSSessionConfig config) { - this.cts = cts; this.config = config; this.transport = new UdpTransport(sender, remote, config.MaxPacketLength); this.protocol = new DtlsServerProtocol(); @@ -44,23 +41,13 @@ public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, Cancel /// public event EventHandler? HandshakeFinished; - /// - /// Cancel the task which handles the the received data. - /// - public void Cancel() - { - this.cts.Cancel(); - } - /// /// Start the task which handles the received data. /// public void Start() { - var th = new Thread(async () => await this.HandleSession()); - th.Start(); // perform handshake asynchronously, would be blocking otherwise - //Task.Run(async () => await this.HandleSession().ConfigureAwait(false)); + Task.Factory.StartNew(this.HandleSession, TaskCreationOptions.LongRunning).ConfigureAwait(false); } /// @@ -91,7 +78,7 @@ public void Enqueue(ReadOnlySpan payload) var length = 0; try { - length = this.dtlsTransport!.Receive(rxBuffer, 1000); + length = this.dtlsTransport!.Receive(rxBuffer, 1); } catch(Exception ex) { @@ -122,7 +109,7 @@ public void Enqueue(ReadOnlySpan payload) } } - private Task HandleSession() + private void HandleSession() { try { @@ -149,7 +136,5 @@ private Task HandleSession() { this.HandshakeFinished?.Invoke(this, new HandshakeFinishedEventArgs() { Successful = !this.HandshakeFailed }); } - - return Task.CompletedTask; } } diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 0872975..89cc2b4 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -20,7 +20,6 @@ public class DTLSSessionManager private readonly IUDPSender sender; private readonly IDTLSFactory factory; private readonly DTLSSessionConfig config; - private readonly CancellationTokenSource cts; private readonly ILogger log = LogManager.GetLogger(); /// @@ -34,7 +33,6 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, IDTLSFactory fa this.sender = sender; this.factory = factory; this.config = config; - this.cts = new CancellationTokenSource(); } /// @@ -64,7 +62,7 @@ public void SendTo(ReadOnlySpan packet, EndPoint endPoint) /// public void Stop() { - this.cts.Cancel(); + } /// @@ -83,7 +81,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) }; entry.PostEvictionCallbacks.Add(callback); - var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, CancellationTokenSource.CreateLinkedTokenSource(this.cts.Token), this.config); + var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, this.config); s.DataReceived += DecryptedReceived; s.HandshakeFinished += HandshakeFinished; this.log.LogInformation("Start DTLS connection with {Remote}", endPoint); @@ -112,7 +110,6 @@ private static void OnEviction(object key, object value, EvictionReason reason, { var obj = value as DTLSSession; LogManager.GetLogger().LogDebug("Session with {Remote} timed out", obj.Remote); - obj?.Cancel(); } } } diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs index 2c7ada6..d79c35f 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -62,7 +62,7 @@ public int Receive(byte[] buf, int off, int len, int waitMillis) /// The amount of received bytes. public int Receive(Span buffer, int waitMillis) { - if (this.sema.Wait(TimeSpan.FromMilliseconds(waitMillis))) + if (this.sema.WaitAsync(waitMillis).GetAwaiter().GetResult()) { if (this.messages.TryDequeue(out var rx)) { diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 68a66f0..082ffd5 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -7,7 +7,7 @@ - + diff --git a/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs b/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs index 3c18a09..046d3f6 100644 --- a/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs +++ b/WorldDirect.CoAP/Deduplication/DeduplicatorFactory.cs @@ -34,7 +34,7 @@ public static IDeduplicator CreateDeduplicator(ICoapConfig config) else if (!String.Equals(NoopDeduplicator, type, StringComparison.OrdinalIgnoreCase) && !String.Equals("NO_DEDUPLICATOR", type, StringComparison.OrdinalIgnoreCase)) { - log.LogWarning("Unknown deduplicator type: " + type); + log?.LogWarning("Unknown deduplicator type: " + type); } return new NoopDeduplicator(); } diff --git a/WorldDirect.CoAP/LinkFormat.cs b/WorldDirect.CoAP/LinkFormat.cs index 0146ecd..baedc4b 100644 --- a/WorldDirect.CoAP/LinkFormat.cs +++ b/WorldDirect.CoAP/LinkFormat.cs @@ -470,7 +470,7 @@ internal static Boolean AddAttribute(ICollection attributes, Link { if (attr.Name.Equals(attrToAdd.Name)) { - log.LogDebug("Found existing singleton attribute: " + attr.Name); + log?.LogDebug("Found existing singleton attribute: " + attr.Name); return false; } } diff --git a/WorldDirect.CoAP/Log/LogManager.cs b/WorldDirect.CoAP/Log/LogManager.cs index b0b710d..5d66942 100644 --- a/WorldDirect.CoAP/Log/LogManager.cs +++ b/WorldDirect.CoAP/Log/LogManager.cs @@ -12,6 +12,7 @@ namespace WorldDirect.CoAP.Log { using System; + using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; /// From df0f8c3a7af7133e7869a5612916e70d721ee655 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 25 Jul 2023 10:36:17 +0200 Subject: [PATCH 07/27] adapt for nuget release --- .../WorldDirect.CoAP.DTLS.csproj | 46 +++++++++++++++---- .../WorldDirect.CoAP.Server.Extensions.csproj | 37 +++++++++++++-- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 3 +- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 082ffd5..1d9b271 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -1,10 +1,40 @@  - - - net6.0 - enable - enable - + + net6.0 + enable + enable + 0.6.0-alpha + World-Direct eBusiness solutions GmbH + 2023 + LICENSE + packageIcon.png + https://github.com/world-direct/CoAP.NET + + Changelog: + + + + true + snupkg + + + + + True + + + + True + + + True + + + + True + + + @@ -12,8 +42,4 @@ - - - - diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 2d9f177..c6d1340 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,17 +4,44 @@ net6.0 enable enable + 0.6.0-alpha + World-Direct eBusiness solutions GmbH + 2023 + LICENSE + packageIcon.png + https://github.com/world-direct/CoAP.NET + + Changelog: + + + true + snupkg + + + + + True + + + + True + + + True + + + + True + + + + - - - - - diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 1d04f34..c7d1c85 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,13 +2,14 @@ netstandard2.0 - 0.5.7 + 0.6.0-alpha World-Direct eBusiness solutions GmbH 2021 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET Changelog: + 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 From 6ac623cc2864b1158cf6ab9530086f2139c8a2dc Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 26 Jul 2023 15:27:09 +0200 Subject: [PATCH 08/27] fix reference --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 4 ++++ .../WorldDirect.CoAP.Server.Extensions.csproj | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 1d9b271..aedbf45 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -42,4 +42,8 @@ + + + + diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index c6d1340..14c6c7f 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -44,4 +44,9 @@ + + + + + From ef03b4664cf7610b481c8fd9e09d255429a01e69 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Mon, 31 Jul 2023 14:38:44 +0200 Subject: [PATCH 09/27] fix configuration --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 12 +- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 12 +- WorldDirect.CoAP.DTLS/DTLSServer.cs | 2 +- WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs | 136 ------------------ WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 59 ++++++++ WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 11 +- WorldDirect.CoAP.DTLS/IDTLSFactory.cs | 13 -- .../WorldDirect.CoAP.DTLS.csproj | 3 +- .../DTLSFactory.cs | 17 --- ...rBuilder.cs => DTLSServerConfigBuilder.cs} | 25 ++-- .../ServiceProviderExtensions.cs | 13 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 4 +- 12 files changed, 104 insertions(+), 203 deletions(-) delete mode 100644 WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs delete mode 100644 WorldDirect.CoAP.DTLS/IDTLSFactory.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs rename WorldDirect.CoAP.Server.Extensions/{DTLSServerBuilder.cs => DTLSServerConfigBuilder.cs} (94%) diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 99a6ad3..dfe952d 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -60,7 +60,7 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox /// Instantiates a new endpoint with the /// specified channel and configuration. /// - public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, ICoapConfig config) + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) { _config = config; _matcher = new Matcher(this._config); @@ -69,20 +69,24 @@ public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, ICoapConfig confi channel.ReceiveBufferSize = this._config.ChannelReceiveBufferSize; channel.SendBufferSize = this._config.ChannelSendBufferSize; channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; - this.channel = new DTLSChannel(channel, cache, factory); + this.channel = new DTLSChannel(channel, cache, dtlsConfig); this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; } - public CoAPSEndpoint(IMemoryCache cache, IDTLSFactory factory, UDPChannel channel, ICoapConfig config) + 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, factory); + this.channel = new DTLSChannel(channel, cache, dtlsConfig); this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; } + public DTLSServerConfig DTLSConfig { get; } + /// public ICoapConfig Config { diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index 52a2d95..7f31800 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -25,14 +25,14 @@ public class DTLSChannel : IChannel /// /// The underlying udp channel used to send/receive data. /// The cache to store dtls sessions. - /// The factory to create dtls server. + /// The configuration of the dtls server. /// The timeout after which a session is deleted. - public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory, TimeSpan sessionTimeout) + 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, }; - this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), factory, config); + this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), dtlsConfig, config); } /// @@ -40,9 +40,9 @@ public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory, /// /// The underlying udp channel used to send/receive data. /// The cache to store dtls sessions. - /// The factory to create dtls server. - public DTLSChannel(UDPChannel channel, IMemoryCache cache, IDTLSFactory factory) - : this(channel, cache, factory, TimeSpan.FromMinutes(2)) + /// The configuration of the dtls server. + public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtlsConfig) + : this(channel, cache, dtlsConfig, TimeSpan.FromMinutes(2)) { } diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index 6d40569..0302953 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -19,7 +19,7 @@ public class DTLSServer : AbstractTlsServer /// /// The cryptostack. /// The configuration of the server. - public DTLSServer(BcTlsCrypto crypto, DTLSServerConfig config) : base(crypto) + public DTLSServer(DTLSServerConfig config) : base(config.Crypto) { this.config = config; this.IsAuthenticated = false; diff --git a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs b/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs deleted file mode 100644 index e22bf9a..0000000 --- a/WorldDirect.CoAP.DTLS/DTLSServerBuilder.cs +++ /dev/null @@ -1,136 +0,0 @@ -namespace WorldDirect.CoAP.DTLS; - -using Org.BouncyCastle.Asn1; -using Org.BouncyCastle.Asn1.X9; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Pkcs; -using Org.BouncyCastle.Security; -using Org.BouncyCastle.Tls; -using Org.BouncyCastle.Tls.Crypto.Impl.BC; -using Org.BouncyCastle.X509; - -/// -/// A helper class to configure the . -/// -/// -/// Limitations: -/// only one ECDSA Chain Certificate work (not RSA, not multiple) -/// -public class DTLSServerBuilder -{ - private readonly BcTlsCrypto crypto; - private Pkcs12Store? store; - private List CAs = new(); - - private readonly DTLSServerConfig config; - - /// - /// Initializes a new instance of the class. - /// - public DTLSServerBuilder() - { - this.crypto = new BcTlsCrypto(new SecureRandom()); - this.config = new DTLSServerConfig(); - } - - /// - /// Add a pkcs12 store where the certificate will be loaded from. - /// - /// Path to the file. - /// Password of the file. - /// The builder. - public DTLSServerBuilder WithStore(string file, string password) - { - var store = new Pkcs12StoreBuilder().Build(); - using var reader = File.OpenRead(file); - store.Load(reader, password.ToCharArray()); - this.store = store; - return this; - } - - /// - /// Set the timeout of the handshake. - /// - /// The timeout. - /// The builder. - public DTLSServerBuilder WithHandShakeTimeout(TimeSpan timeout) - { - this.config.HandshakeTimeout = timeout; - return this; - } - - /// - /// Loads the certificate chain and its private key from the store. The store must be loaded before with function. - /// - /// The alias which identifies the certificate. - /// - public DTLSServerBuilder WithEcdsaCertificate(string alias) - { - // check if certificate is ecdsa certificate. - var certificateChain = this.store!.GetCertificateChain(alias); - var key = this.store!.GetKey(alias); - - var serverCert = certificateChain[0].Certificate; - var der = new DerObjectIdentifier(serverCert.SigAlgOid); - if (!der.On(X9ObjectIdentifiers.id_ecSigType)) - { - // signature algorithm of certificate is not ECDSA - throw new InvalidOperationException( - $"Provided certificate of {alias} was signed with {serverCert.SigAlgName}. This not an ECDSA algorithm"); - } - if (key.Key.GetType() != typeof(ECPrivateKeyParameters)) - { - throw new InvalidOperationException($"Provided key of {alias} is not an ECKey"); - } - - var x509Certs = certificateChain.Select(c => this.crypto.CreateCertificate(c.Certificate.GetEncoded())); - - var ecPrivateKey = (key.Key as ECPrivateKeyParameters)!; - var ecdsaCertificate = new Certificate(x509Certs.ToArray()); - - this.config.EcCertificate = new EcServerCertificate(ecdsaCertificate, ecPrivateKey); - return this; - } - - /// - /// Loads a trusted root CA from a pem encoded file. - /// - /// The filename fo load the CA from. - /// - /// - /// - public DTLSServerBuilder WithTrustedRoot(string filename) - { - X509CertificateParser parser = new X509CertificateParser(); - using var file = File.Open(filename, FileMode.Open); - var cert = parser.ReadCertificate(file); - if (cert == null) - { - throw new InvalidOperationException($"Could not read certificate from {filename}"); - } - - this.config.CAs.Add(cert); - return this; - } - - /// - /// Adds enabled ciphersuites to the server. - /// - /// The cipher suites to add. - /// The builder - public DTLSServerBuilder WithCipherSuites(IEnumerable suites) - { - this.config.CipherSuites.AddRange(suites); - // todo check if possibility to check if selected suites are valid with current configuration (ECDSA & RSA avaiable? configured psk?) - return this; - } - - /// - /// Builds the based on the configuration. - /// - /// The configured . - public DTLSServer Build() - { - return new DTLSServer(this.crypto, this.config); - } -} diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs index b64cfb9..e432886 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -1,6 +1,7 @@ namespace WorldDirect.CoAP.DTLS; using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; using Org.BouncyCastle.X509; /// @@ -8,6 +9,64 @@ /// 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; + } + + /// + /// 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; } +} + +/// +/// 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. /// diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 89cc2b4..b27bfc5 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Org.BouncyCastle.Asn1.Nist; using Org.BouncyCastle.Asn1.X509; + using Org.BouncyCastle.Tls.Crypto.Impl.BC; using WorldDirect.CoAP.Log; /// @@ -18,7 +19,7 @@ public class DTLSSessionManager { private readonly IMemoryCache cache; private readonly IUDPSender sender; - private readonly IDTLSFactory factory; + private readonly DTLSServerConfig dtlsServerConfig; private readonly DTLSSessionConfig config; private readonly ILogger log = LogManager.GetLogger(); @@ -26,12 +27,14 @@ public class DTLSSessionManager /// 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, IDTLSFactory factory, DTLSSessionConfig config) + public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfig dtlsServerConfig, DTLSSessionConfig config) { this.cache = cache; this.sender = sender; - this.factory = factory; + this.dtlsServerConfig = dtlsServerConfig; this.config = config; } @@ -81,7 +84,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) }; entry.PostEvictionCallbacks.Add(callback); - var s = new DTLSSession(this.sender, this.factory.CreateServer(), endPoint, this.config); + var s = new DTLSSession(this.sender, new DTLSServer(this.dtlsServerConfig), endPoint, this.config); s.DataReceived += DecryptedReceived; s.HandshakeFinished += HandshakeFinished; this.log.LogInformation("Start DTLS connection with {Remote}", endPoint); diff --git a/WorldDirect.CoAP.DTLS/IDTLSFactory.cs b/WorldDirect.CoAP.DTLS/IDTLSFactory.cs deleted file mode 100644 index 5981e87..0000000 --- a/WorldDirect.CoAP.DTLS/IDTLSFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace WorldDirect.CoAP.DTLS; - -/// -/// An interface to create a . -/// -public interface IDTLSFactory -{ - /// - /// Create a new instance. - /// - /// The newly created . - public DTLSServer CreateServer(); -} diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index aedbf45..ef0c51d 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.0-alpha + 0.6.1-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -11,6 +11,7 @@ https://github.com/world-direct/CoAP.NET Changelog: + v0.6.1: adapt interface to interact with DTLS configuration diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs b/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs deleted file mode 100644 index f8b2375..0000000 --- a/WorldDirect.CoAP.Server.Extensions/DTLSFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions; - -using DTLS; - -internal class DTLSFactory : IDTLSFactory -{ - private readonly DTLSServerBuilder builder; - - public DTLSFactory(DTLSServerBuilder builder) - { - this.builder = builder; - } - public DTLSServer CreateServer() - { - return this.builder.Build(); - } -} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs b/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs similarity index 94% rename from WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs rename to WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs index b2cc429..12fc0fe 100644 --- a/WorldDirect.CoAP.Server.Extensions/DTLSServerBuilder.cs +++ b/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs @@ -10,25 +10,28 @@ using Org.BouncyCastle.Tls; using Org.BouncyCastle.Tls.Crypto.Impl.BC; -public class DTLSServerBuilder +public class DTLSServerConfigBuilder { // TODO: Check if certificate usage is allowed for server auth when loaded from files // TODO: Check if CA is allowed to be used for (KeyCertSign) when loaded from file private readonly BcTlsCrypto crypto; - private readonly DTLSServerConfig config; - public DTLSServerBuilder() + private readonly MutableDTLSServerConfig config; + public DTLSServerConfigBuilder() { this.crypto = new BcTlsCrypto(new SecureRandom()); - this.config = new DTLSServerConfig(); + this.config = new MutableDTLSServerConfig(); + this.config.Crypto = this.crypto; } + public DTLSServerConfig Config => new (this.config); + /// /// Loads the servers certificate based on the configuration settings. /// /// The settings to identify the certificate. /// /// - public DTLSServerBuilder SetCertificate(CertificateConfig config) + public DTLSServerConfigBuilder SetCertificate(CertificateConfig config) { if (config.IsFromStore) { @@ -71,7 +74,7 @@ public DTLSServerBuilder SetCertificate(CertificateConfig config) return this; } - public DTLSServerBuilder AddCA(CertificateConfig config) + public DTLSServerConfigBuilder AddCA(CertificateConfig config) { if (config.IsFromStore) { @@ -110,13 +113,13 @@ public DTLSServerBuilder AddCA(CertificateConfig config) return this; } - public DTLSServerBuilder SetHandshakeTimeout(TimeSpan timeout) + public DTLSServerConfigBuilder SetHandshakeTimeout(TimeSpan timeout) { this.config.HandshakeTimeout = timeout; return this; } - public DTLSServerBuilder SetPskManager(TlsPskIdentityManager manager) + public DTLSServerConfigBuilder SetPskManager(TlsPskIdentityManager manager) { this.config.PskManager = manager; this.config.CipherSuites.Add(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8); @@ -124,12 +127,6 @@ public DTLSServerBuilder SetPskManager(TlsPskIdentityManager manager) return this; } - - public DTLSServer Build() - { - return new DTLSServer(this.crypto, this.config); - } - private Org.BouncyCastle.X509.X509Certificate LoadCertFromFile(string filename) { using var certReader = File.OpenRead(filename); diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index 1967c9a..217a36a 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -60,16 +60,19 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura foreach (var listenEndpoint in options.ListenOptions) { - if (listenEndpoint!.EndpointConfig.CertificateConfig == null) + if (listenEndpoint!.EndpointConfig.CertificateConfig == null && !listenEndpoint.EndpointConfig.Url.StartsWith("coaps")) { // insecure server.AddEndPoint(listenEndpoint.Endpoint as IPEndPoint); } else { - var dtlsServerBuilder = new DTLSServerBuilder() - .SetCertificate(listenEndpoint.EndpointConfig.CertificateConfig) - .SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); + var dtlsServerBuilder = new DTLSServerConfigBuilder(); + if (listenEndpoint.EndpointConfig.CertificateConfig != null) + { + dtlsServerBuilder.SetCertificate(listenEndpoint.EndpointConfig.CertificateConfig); + } + dtlsServerBuilder.SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); var resolver = serviceProvider.GetService(); if (resolver != null) { @@ -96,7 +99,7 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura channel.SendBufferSize = config.ChannelSendBufferSize; channel.ReceivePacketSize = config.ChannelReceivePacketSize; - var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), new DTLSFactory(dtlsServerBuilder), channel, config); + var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), dtlsServerBuilder.Config, channel, config); server.AddEndPoint(ep); } diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 14c6c7f..323ff9c 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.0-alpha + 0.6.1-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -12,6 +12,7 @@ https://github.com/world-direct/CoAP.NET Changelog: + v0.6.1: adapt to easier configuration of dtls @@ -46,7 +47,6 @@ - From f1d5da8da8286bb4e3162f4ad60a4d22667b6d1a Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Thu, 3 Aug 2023 15:08:49 +0200 Subject: [PATCH 10/27] add SLL Keyfile exporter --- EnergySoultions.CoAP.sln | 20 +++++- WorldDirect.CoAP.DTLS/DTLS12KeyFileData.cs | 65 +++++++++++++++++++ WorldDirect.CoAP.DTLS/DTLSServer.cs | 14 +++- WorldDirect.CoAP.DTLS/DTLSServerConfig.cs | 38 ++--------- WorldDirect.CoAP.DTLS/IKeyStore.cs | 20 ++++++ .../MutableDTLSServerConfig.cs | 52 +++++++++++++++ WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs | 44 +++++++++++++ .../WorldDirect.CoAP.DTLS.csproj | 3 +- .../DTLSServerConfigBuilder.cs | 12 ++++ .../ServiceProviderExtensions.cs | 7 ++ .../WorldDirect.CoAP.Server.Extensions.csproj | 3 +- .../DTLS12KeyFileDataSpecs.cs | 22 +++++++ WorldDirect.CoAPS.DTLS.Specs/Usings.cs | 1 + .../WorldDirect.CoAPS.DTLS.Specs.csproj | 30 +++++++++ 14 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 WorldDirect.CoAP.DTLS/DTLS12KeyFileData.cs create mode 100644 WorldDirect.CoAP.DTLS/IKeyStore.cs create mode 100644 WorldDirect.CoAP.DTLS/MutableDTLSServerConfig.cs create mode 100644 WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs create mode 100644 WorldDirect.CoAPS.DTLS.Specs/DTLS12KeyFileDataSpecs.cs create mode 100644 WorldDirect.CoAPS.DTLS.Specs/Usings.cs create mode 100644 WorldDirect.CoAPS.DTLS.Specs/WorldDirect.CoAPS.DTLS.Specs.csproj diff --git a/EnergySoultions.CoAP.sln b/EnergySoultions.CoAP.sln index 0500eb4..c06f89e 100644 --- a/EnergySoultions.CoAP.sln +++ b/EnergySoultions.CoAP.sln @@ -21,11 +21,13 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.DTLS", "WorldDirect.CoAP.DTLS\WorldDirect.CoAP.DTLS.csproj", "{22366801-AB3F-4F99-AD26-80B6755F0709}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.DTLS", "WorldDirect.CoAP.DTLS\WorldDirect.CoAP.DTLS.csproj", "{22366801-AB3F-4F99-AD26-80B6755F0709}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.Server.Extensions", "WorldDirect.CoAP.Server.Extensions\WorldDirect.CoAP.Server.Extensions.csproj", "{7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Server.Extensions", "WorldDirect.CoAP.Server.Extensions\WorldDirect.CoAP.Server.Extensions.csproj", "{7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorldDirect.CoAP.Server.Extensions.Specs", "WorldDirect.CoAP.Server.Extensions.Specs\WorldDirect.CoAP.Server.Extensions.Specs.csproj", "{C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Server.Extensions.Specs", "WorldDirect.CoAP.Server.Extensions.Specs\WorldDirect.CoAP.Server.Extensions.Specs.csproj", "{C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}" +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 @@ -121,6 +123,18 @@ Global {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/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/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index 0302953..a0ffc92 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -1,9 +1,11 @@ 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; @@ -17,7 +19,6 @@ public class DTLSServer : AbstractTlsServer /// /// Initializes a new instance of the class. /// - /// The cryptostack. /// The configuration of the server. public DTLSServer(DTLSServerConfig config) : base(config.Crypto) { @@ -45,6 +46,9 @@ public TlsCertificate? PeerCertificate } } + /// + /// Gets the used PSK identity of the remote. + /// public byte[] PskIdentity { get; private set; } = Array.Empty(); /// @@ -81,6 +85,14 @@ public override void NotifyHandshakeComplete() 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() diff --git a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs index e432886..30b1b01 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServerConfig.cs @@ -21,6 +21,7 @@ public DTLSServerConfig(MutableDTLSServerConfig config) this.CipherSuites = config.CipherSuites; this.HandshakeTimeout = config.HandshakeTimeout; this.PskManager = config.PskManager; + this.KeyStore = config.KeyStore; } /// @@ -55,43 +56,12 @@ public DTLSServerConfig(MutableDTLSServerConfig config) /// Gets or sets the provider for psk keys. /// public TlsPskIdentityManager? PskManager { get; } -} - -/// -/// 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. + /// Gets the store where the session keys should be stored. /// /// - /// 0 means no timeout + /// !!! ATTENTION !!! Will export session keys of communication! Only use in DEV environment. /// - public TimeSpan HandshakeTimeout { get; set; } = TimeSpan.Zero; - - /// - /// Gets or sets the provider for psk keys. - /// - public TlsPskIdentityManager? PskManager { get; set; } + public IKeyStore? KeyStore { get; } } 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/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..b643cda --- /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.OpenOrCreate); + 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/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index ef0c51d..cceb8af 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.1-alpha + 0.6.2-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -11,6 +11,7 @@ https://github.com/world-direct/CoAP.NET Changelog: + v0.6.2: add session key file exporter v0.6.1: adapt interface to interact with DTLS configuration diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs b/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs index 12fc0fe..68b6c7d 100644 --- a/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs +++ b/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs @@ -119,6 +119,18 @@ public DTLSServerConfigBuilder SetHandshakeTimeout(TimeSpan timeout) return this; } + /// + /// Add an exporter of the session keys. + /// + /// ATTENTION!! will export session keys. Only use in development. + /// The store where the keys should be exported. + /// The builder. + public DTLSServerConfigBuilder EnableExportOfSessionKeys(IKeyStore store) + { + this.config.KeyStore = store; + return this; + } + public DTLSServerConfigBuilder SetPskManager(TlsPskIdentityManager manager) { this.config.PskManager = manager; diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index 217a36a..e86ff0f 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Channel; using Configuration; + using DTLS; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -83,6 +84,12 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura } } + var keyStore = serviceProvider.GetService(); + if (keyStore != null) + { + dtlsServerBuilder.EnableExportOfSessionKeys(keyStore); + } + foreach (var ca in listenEndpoint.EndpointConfig.ClientCAs) { dtlsServerBuilder.AddCA(ca); diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 323ff9c..58e3ff7 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.1-alpha + 0.6.2-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -12,6 +12,7 @@ https://github.com/world-direct/CoAP.NET Changelog: + v0.6.2: add ssl keyfile for extraction of session keys v0.6.1: adapt to easier configuration of dtls 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 + + + + + + + + From f4dcf0b9b344cd0bc95ef7a8bc01c876b4705113 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Mon, 7 Aug 2023 15:13:11 +0200 Subject: [PATCH 11/27] fix versioning --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index cceb8af..f589c83 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.2-alpha + 0.6.2-beta World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 58e3ff7..87a70b5 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.2-alpha + 0.6.2-beta World-Direct eBusiness solutions GmbH 2023 LICENSE From 90d0be13a2c7dc55329e040780be7accfdf3a627 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Fri, 11 Aug 2023 23:33:10 +0200 Subject: [PATCH 12/27] fix one off receiving error --- WorldDirect.CoAP.DTLS/DTLSSession.cs | 47 ++++++++++++++++++---------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 81dea34..a50de45 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -1,5 +1,6 @@ namespace WorldDirect.CoAP.DTLS; +using System; using System.Data; using System.Net; using System.Security.Cryptography.X509Certificates; @@ -78,7 +79,14 @@ public void Enqueue(ReadOnlySpan payload) var length = 0; try { - length = this.dtlsTransport!.Receive(rxBuffer, 1); + do + { + length = this.dtlsTransport!.Receive(rxBuffer, 1); + if (length > 0) + { + this.InvokeDataReceived(rxBuffer.Take(length).ToArray()); + } + } while (length > 0); } catch(Exception ex) { @@ -87,28 +95,33 @@ public void Enqueue(ReadOnlySpan payload) if (length > 0) { - if (this.dtlsServer.IsAuthenticated) - { - if (this.dtlsServer.PeerCertificate != null) - { - var peerCert = new X509Certificate(this.dtlsServer.PeerCertificate.GetEncoded()); - this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, peerCert)); - } - else if (this.dtlsServer.PskIdentity.Any()) - { - this.DataReceived?.Invoke(this, new DTLSDataReceivedEventArgs(rxBuffer.Take(length).ToArray(), this.transport.Remote, Encoding.ASCII.GetString(this.dtlsServer.PskIdentity))); - } - } - else - { - throw new NotImplementedException($"Unauthenticated communication is not implemented"); - } + } } } + 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 From b901dff4707de762d493da4e6b925d44e1c20883 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Fri, 11 Aug 2023 23:34:04 +0200 Subject: [PATCH 13/27] add tracing diagnostics to requests --- WorldDirect.CoAP/Net/Exchange.cs | 50 +++++++++++++++++++ .../Server/Resources/CoapExchange.cs | 3 ++ WorldDirect.CoAP/Stack/BlockwiseLayer.cs | 1 + WorldDirect.CoAP/Tracing.cs | 20 ++++++++ WorldDirect.CoAP/WorldDirect.CoAP.csproj | 3 +- 5 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 WorldDirect.CoAP/Tracing.cs diff --git a/WorldDirect.CoAP/Net/Exchange.cs b/WorldDirect.CoAP/Net/Exchange.cs index b3cf9ce..b34cb2f 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; @@ -29,6 +30,7 @@ public class Exchange { private readonly ConcurrentDictionary _attributes = new ConcurrentDictionary(); private readonly Origin _origin; + private readonly Activity activity; private Boolean _timedOut; private Request _request; private Request _currentRequest; @@ -51,6 +53,52 @@ public Exchange(Request request, Origin origin) _origin = origin; _currentRequest = request; _timestamp = DateTime.Now; + if (origin == Origin.Local) + { + this.activity = Tracing.ClientSource.StartActivity("CoAP Request"); + } + else + { + Activity.Current = null; + this.activity = Tracing.ServerSource.CreateActivity("CoAP Request", 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); + if (origin == Origin.Local) + { + this.activity?.AddTag("coap.remote", request.Destination); + } + + request.Retransmitting += (obj, ev) => this.activity?.AddEvent(new ActivityEvent("Retransmitting")); + request.Respond += (obj, ev) => + { + this.activity?.AddTag("coap.statuscode", ev.Response.StatusCode); + this.activity?.Stop(); + }; + request.TimedOut += (obj, ev) => + { + this.activity?.AddTag("coap.statuscode", "TIMEOUT"); + this.activity?.Stop(); + }; + request.Rejected += (obj, ev) => + { + this.activity?.AddTag("coap.statuscode", "REJECTED"); + this.activity?.Stop(); + }; + request.Responding += (obj, ev) => + { + var response = ev.Response; + if (response.Block1 != null) + { + this.activity?.AddEvent(new ActivityEvent($"New Block {ev.Response.Block1?.NUM}")); + } + else if (response.Block2 != null) + { + this.activity?.AddEvent(new ActivityEvent($"New Block {ev.Response.Block2?.NUM}")); + } + }; } public Origin Origin @@ -58,6 +106,8 @@ public Origin Origin get { return _origin; } } + public Activity Activity => this.activity; + /// /// Gets or sets the endpoint which has created and processed this exchange. /// diff --git a/WorldDirect.CoAP/Server/Resources/CoapExchange.cs b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs index 8d47c9a..b6ed997 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; /// @@ -44,6 +45,8 @@ public T Get(Object key) return this._exchange.Get(key); } + public Activity Activity => this._exchange.Activity; + /// /// Gets the request. /// diff --git a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs index 2c072ec..564189f 100644 --- a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs +++ b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs @@ -242,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); diff --git a/WorldDirect.CoAP/Tracing.cs b/WorldDirect.CoAP/Tracing.cs new file mode 100644 index 0000000..97409a7 --- /dev/null +++ b/WorldDirect.CoAP/Tracing.cs @@ -0,0 +1,20 @@ +namespace WorldDirect.CoAP +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + + internal static class Tracing + { + /// + /// The activity source for the tracing client events. + /// + internal static readonly ActivitySource ClientSource = new ActivitySource("WorldDirect.CoAP.Client", "1.0.0"); + + /// + /// The activity source for the tracing events. + /// + internal static readonly ActivitySource ServerSource = new ActivitySource("WorldDirect.CoAP.Server", "1.0.0"); + } +} diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index c7d1c85..5e5aac4 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -43,6 +43,7 @@ + From 1404d4c0c7a1c5cb26821904642407701525fb58 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 22 Aug 2023 13:51:42 +0200 Subject: [PATCH 14/27] adapt versioning --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 9 +++++---- .../WorldDirect.CoAP.Server.Extensions.csproj | 9 +++++---- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index f589c83..e66e686 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,16 +3,17 @@ net6.0 enable enable - 0.6.2-beta + 0.6.3-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET - Changelog: - v0.6.2: add session key file exporter - v0.6.1: adapt interface to interact with DTLS configuration + 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 diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 87a70b5..b76654e 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,16 +4,17 @@ net6.0 enable enable - 0.6.2-beta + 0.6.3-alpha World-Direct eBusiness solutions GmbH 2023 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET - Changelog: - v0.6.2: add ssl keyfile for extraction of session keys - v0.6.1: adapt to easier configuration of dtls + 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 diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 5e5aac4..8b4ba08 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,13 +2,14 @@ netstandard2.0 - 0.6.0-alpha + 0.6.1-alpha World-Direct eBusiness solutions GmbH 2021 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET Changelog: + v0.6.1: add tracing diagnostics v0.6.0: adaption of logging v0.5.7: Bugfix blockwise transfer v0.5.6: Add progress reporting on long running Put From 27e1a3e032bf653c1eae7ed509bbfe123ab70916 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 29 Aug 2023 16:27:20 +0200 Subject: [PATCH 15/27] add some dtls metrics --- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 2 +- WorldDirect.CoAP.DTLS/DTLSMetrics.cs | 77 +++++++++++++++++++ WorldDirect.CoAP.DTLS/DTLSSession.cs | 10 +-- WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs | 5 ++ WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 3 + .../WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 3 +- WorldDirect.CoAP/Net/Exchange.cs | 4 +- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 9 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 WorldDirect.CoAP.DTLS/DTLSMetrics.cs diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index 7f31800..79bc6c1 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -31,7 +31,7 @@ public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtls { this.channel = channel; this.channel.DataReceived += DtlsReceived; - var config = new DTLSSessionConfig() { MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = sessionTimeout, }; + var config = new DTLSSessionConfig() { MaxPacketLength = channel.ReceivePacketSize, SessionTimeout = sessionTimeout, HandshakeTimeout = dtlsConfig.HandshakeTimeout,}; this.sessionManager = new DTLSSessionManager(cache, new UdpChannelSender(channel), dtlsConfig, config); } 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/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index a50de45..5dddc26 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -76,7 +76,7 @@ public void Enqueue(ReadOnlySpan payload) if (dtlsTransport != null) { var rxBuffer = new byte[this.config.MaxPacketLength]; - var length = 0; + int length; try { do @@ -92,12 +92,6 @@ public void Enqueue(ReadOnlySpan payload) { this.logger.LogTrace(ex, "Cant receive decrypted dtls packet from {Remote}", this.Remote); } - - if (length > 0) - { - - - } } } @@ -133,7 +127,7 @@ private void HandleSession() catch (TlsTimeoutException e) { this.HandshakeFailed = true; - this.logger.LogError(e, "{Remote} failed handshake because of timeout", this.Remote); + this.logger.LogError(e, "{Remote} failed handshake because of timeout ({Timeout})", this.Remote, this.config.HandshakeTimeout); } catch (TlsFatalAlert e) { diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs index 6d0cd94..72d133d 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionConfig.cs @@ -14,4 +14,9 @@ public class DTLSSessionConfig /// 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 index b27bfc5..ed2eb36 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -89,6 +89,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) s.HandshakeFinished += HandshakeFinished; this.log.LogInformation("Start DTLS connection with {Remote}", endPoint); s.Start(); + DTLSMetrics.Log.SessionAdded(); return s; }); @@ -100,6 +101,7 @@ private void HandshakeFinished(object? sender, HandshakeFinishedEventArgs e) var session = (sender as DTLSSession)!; if (!e.Successful) { + DTLSMetrics.Log.HandshakeFailed(); this.cache.Remove(session.Remote.ToString()); } } @@ -113,6 +115,7 @@ private static void OnEviction(object key, object value, EvictionReason reason, { var obj = value as DTLSSession; LogManager.GetLogger().LogDebug("Session with {Remote} timed out", obj.Remote); + DTLSMetrics.Log.SessionRemoved(); } } } diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index e66e686..80c5d23 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha + 0.6.3-alpha4 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index b76654e..874a556 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha + 0.6.3-alpha4 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -49,6 +49,7 @@ + diff --git a/WorldDirect.CoAP/Net/Exchange.cs b/WorldDirect.CoAP/Net/Exchange.cs index b34cb2f..58c85c1 100644 --- a/WorldDirect.CoAP/Net/Exchange.cs +++ b/WorldDirect.CoAP/Net/Exchange.cs @@ -30,7 +30,7 @@ public class Exchange { private readonly ConcurrentDictionary _attributes = new ConcurrentDictionary(); private readonly Origin _origin; - private readonly Activity activity; + private Activity activity; private Boolean _timedOut; private Request _request; private Request _currentRequest; @@ -217,6 +217,8 @@ public Boolean Complete if (value) { Completed?.Invoke(this, EventArgs.Empty); + this.Activity?.Stop(); + this.activity = null; } } } diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 8b4ba08..f72560c 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 0.6.1-alpha + 0.6.1-alpha1 World-Direct eBusiness solutions GmbH 2021 LICENSE From d9482b1c6937e08c44e44aaecbab8ce205dee4d5 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Thu, 31 Aug 2023 11:24:04 +0200 Subject: [PATCH 16/27] change logging level --- WorldDirect.CoAP/Net/Matcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WorldDirect.CoAP/Net/Matcher.cs b/WorldDirect.CoAP/Net/Matcher.cs index b83a0f1..56f17ac 100644 --- a/WorldDirect.CoAP/Net/Matcher.cs +++ b/WorldDirect.CoAP/Net/Matcher.cs @@ -336,7 +336,7 @@ public Exchange ReceiveResponse(Response response) } else { - log.LogInformation("Ignoring unmatchable piggy-backed response from " + response.Source + ": " + response); + log.LogTrace("Ignoring unmatchable piggy-backed response from " + response.Source + ": " + response); } // ignore response return null; From d71fc75daa4b882be721aa92e211b15d8f0537c3 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 17 Oct 2023 16:45:57 +0200 Subject: [PATCH 17/27] finalize diagnostics --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 19 +- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 9 +- WorldDirect.CoAP.DTLS/DTLSServer.cs | 28 +++ WorldDirect.CoAP.DTLS/DTLSSession.cs | 42 +++-- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 26 +-- WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs | 2 +- WorldDirect.CoAP.DTLS/UdpChannelSender.cs | 4 + WorldDirect.CoAP.DTLS/UdpTransport.cs | 15 +- .../WorldDirect.CoAP.DTLS.csproj | 4 +- .../ServiceProviderExtensions.cs | 18 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 4 +- WorldDirect.CoAP/Channel/UDPChannel.cs | 2 + WorldDirect.CoAP/CoapClient.cs | 35 +++- WorldDirect.CoAP/Metrics.cs | 69 +++++++ WorldDirect.CoAP/Net/Exchange.cs | 51 ------ .../Server/Resources/AsyncResource.cs | 172 ++++++++++++++++++ .../Server/Resources/CoapExchange.cs | 16 +- .../Server/ServerMessageDeliverer.cs | 5 + WorldDirect.CoAP/WorldDirect.CoAP.csproj | 4 +- 19 files changed, 413 insertions(+), 112 deletions(-) create mode 100644 WorldDirect.CoAP/Metrics.cs create mode 100644 WorldDirect.CoAP/Server/Resources/AsyncResource.cs diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index dfe952d..d672241 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -18,6 +18,7 @@ namespace WorldDirect.CoAP.Net using Channel; using Codec; using DTLS; + using LazyCache; using Log; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -60,7 +61,7 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox /// Instantiates a new endpoint with the /// specified channel and configuration. /// - public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) + public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) { _config = config; _matcher = new Matcher(this._config); @@ -74,7 +75,7 @@ public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfi this.DTLSConfig = dtlsConfig; } - public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, UDPChannel channel, ICoapConfig config) + public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, UDPChannel channel, ICoapConfig config) { _config = config; _matcher = new Matcher(this._config); @@ -199,6 +200,16 @@ 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) { @@ -243,7 +254,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) { exchange.EndPoint = this; exchange.Set(nameof(DTLSClientAuthentication), e.ClientAuthentication); - _coapStack.ReceiveRequest(exchange, request); + this.ReceiveRequest(exchange, request); } } } @@ -261,7 +272,7 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) { response.RTT = (DateTime.Now - exchange.Timestamp).TotalMilliseconds; exchange.EndPoint = this; - _coapStack.ReceiveResponse(exchange, response); + this.ReceiveResponse(exchange, response); } else if (response.Type != MessageType.ACK) { diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index 79bc6c1..6d5a4ae 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; + using LazyCache; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using WorldDirect.CoAP.Log; @@ -27,7 +28,7 @@ public class DTLSChannel : IChannel /// 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) + public DTLSChannel(UDPChannel channel, IAppCache cache, DTLSServerConfig dtlsConfig, TimeSpan sessionTimeout) { this.channel = channel; this.channel.DataReceived += DtlsReceived; @@ -41,14 +42,14 @@ public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtls /// 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) + public DTLSChannel(UDPChannel channel, IAppCache cache, DTLSServerConfig dtlsConfig) : this(channel, cache, dtlsConfig, TimeSpan.FromMinutes(2)) { } private void DtlsReceived(object? sender, DataReceivedEventArgs e) { - this.sessionManager.ReceivedUdpPacket(e.Data, e.EndPoint); + Task.Factory.StartNew(() => this.sessionManager.ReceivedUdpPacket(e.Data, e.EndPoint)).ConfigureAwait(false); } /// @@ -85,7 +86,7 @@ public void Stop() /// public void Send(byte[] data, EndPoint ep) { - this.logger.LogTrace("Sending {Bytes} udp bytes to {Remote}", data.Length, ep); + this.logger.LogTrace("Sending {Bytes} decrypted bytes to {Remote}", data.Length, ep); this.sessionManager.SendTo(data, ep); } diff --git a/WorldDirect.CoAP.DTLS/DTLSServer.cs b/WorldDirect.CoAP.DTLS/DTLSServer.cs index a0ffc92..232f612 100644 --- a/WorldDirect.CoAP.DTLS/DTLSServer.cs +++ b/WorldDirect.CoAP.DTLS/DTLSServer.cs @@ -51,6 +51,34 @@ public TlsCertificate? PeerCertificate /// 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. /// diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 5dddc26..912771a 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -57,7 +57,15 @@ public void Start() /// The plaintext payload. public void Send(ReadOnlySpan payload) { - this.dtlsTransport?.Send(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); + } } /// @@ -75,24 +83,26 @@ public void Enqueue(ReadOnlySpan payload) // if handshake was was performed successfully, decrypt data directly if (dtlsTransport != null) { - var rxBuffer = new byte[this.config.MaxPacketLength]; - int length; - try + lock (this.dtlsTransport) { - do + var rxBuffer = new byte[this.config.MaxPacketLength]; + int length; + try { - length = this.dtlsTransport!.Receive(rxBuffer, 1); - if (length > 0) + do { - this.InvokeDataReceived(rxBuffer.Take(length).ToArray()); - } - } while (length > 0); - } - catch(Exception ex) - { - this.logger.LogTrace(ex, "Cant receive decrypted dtls packet from {Remote}", this.Remote); + 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); + } } - } } @@ -122,7 +132,7 @@ private void HandleSession() { // perform handshake this.dtlsTransport = this.protocol.Accept(this.dtlsServer, this.transport); - this.logger.LogInformation("{Remote} finished handshake successfully", this.Remote); + this.logger.LogInformation("Finished handshake with {Remote} successfully", this.Remote); } catch (TlsTimeoutException e) { diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index ed2eb36..ddc7e41 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; using Channel; + using LazyCache; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -17,7 +18,7 @@ /// public class DTLSSessionManager { - private readonly IMemoryCache cache; + private readonly IAppCache cache; private readonly IUDPSender sender; private readonly DTLSServerConfig dtlsServerConfig; private readonly DTLSSessionConfig config; @@ -30,7 +31,7 @@ public class DTLSSessionManager /// 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) + public DTLSSessionManager(IAppCache cache, IUDPSender sender, DTLSServerConfig dtlsServerConfig, DTLSSessionConfig config) { this.cache = cache; this.sender = sender; @@ -50,14 +51,15 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfi /// The remote endpoint. public void SendTo(ReadOnlySpan packet, EndPoint endPoint) { - if(this.cache.TryGetValue(endPoint.ToString(), out var session)) + // cache.TryGetValue does not work (always returns false with null object...) + var session = this.cache.Get(endPoint.ToString()); + if (session != null) { session.Send(packet); + return; } - else - { - this.log.LogWarning("Tried to send data to {Remote} but no session available", endPoint); - } + + this.log.LogWarning("Tried to send data to {Remote} but no session available", endPoint); } /// @@ -75,24 +77,25 @@ public void Stop() /// The endpoint who sent the packet. internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { - var session = this.cache.GetOrCreate(endPoint.ToString(), entry => + var session = this.cache.GetOrAdd(endPoint.ToString(), entry => { entry.SlidingExpiration = config.SessionTimeout; var callback = new PostEvictionCallbackRegistration() { EvictionCallback = OnEviction, + State = this }; entry.PostEvictionCallbacks.Add(callback); var s = new DTLSSession(this.sender, new DTLSServer(this.dtlsServerConfig), endPoint, this.config); s.DataReceived += DecryptedReceived; s.HandshakeFinished += HandshakeFinished; - this.log.LogInformation("Start DTLS connection with {Remote}", endPoint); + this.log.LogDebug("Start DTLS connection with {Remote}", endPoint); s.Start(); DTLSMetrics.Log.SessionAdded(); return s; }); - + this.log.LogTrace("Received {Bytes} encrypted Bytes from {Remote}", packet.Length, endPoint); session.Enqueue(packet); } @@ -113,8 +116,9 @@ private void DecryptedReceived(object? _, DTLSDataReceivedEventArgs e) private static void OnEviction(object key, object value, EvictionReason reason, object state) { + var manager = (DTLSSessionManager)state; var obj = value as DTLSSession; - LogManager.GetLogger().LogDebug("Session with {Remote} timed out", obj.Remote); + manager.log.LogDebug("Session with {Remote} timed out", obj.Remote); DTLSMetrics.Log.SessionRemoved(); } } diff --git a/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs b/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs index b643cda..834c2af 100644 --- a/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs +++ b/WorldDirect.CoAP.DTLS/SSLKeyFileStore.cs @@ -31,7 +31,7 @@ public void Store(DTLS12KeyFileData data) { try { - using var file = File.Open(this.fileName, FileMode.OpenOrCreate); + 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)}"); } diff --git a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs index 237a91d..271448c 100644 --- a/WorldDirect.CoAP.DTLS/UdpChannelSender.cs +++ b/WorldDirect.CoAP.DTLS/UdpChannelSender.cs @@ -2,6 +2,8 @@ using System.Net; using Channel; +using Log; +using Microsoft.Extensions.Logging; /// /// Represents a udp sender using a CoAP UDP channel. @@ -9,6 +11,7 @@ public class UdpChannelSender : IUDPSender { private readonly UDPChannel channel; + private readonly ILogger logger = LogManager.GetLogger(); /// /// Initializes a new instance of the class. @@ -22,6 +25,7 @@ public UdpChannelSender(UDPChannel 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 index d79c35f..2d8e874 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -11,8 +11,7 @@ internal class UdpTransport : DatagramTransport { private readonly IUDPSender sender; private readonly int maxPacketLength; - private readonly ConcurrentQueue messages = new (); - private readonly SemaphoreSlim sema = new (0); + private readonly BlockingCollection messages = new (); /// /// Initialize a new instance of the class. @@ -62,13 +61,10 @@ public int Receive(byte[] buf, int off, int len, int waitMillis) /// The amount of received bytes. public int Receive(Span buffer, int waitMillis) { - if (this.sema.WaitAsync(waitMillis).GetAwaiter().GetResult()) + if (this.messages.TryTake(out var rx, TimeSpan.FromMilliseconds(waitMillis))) { - if (this.messages.TryDequeue(out var rx)) - { - rx.CopyTo(buffer); - return rx.Length > buffer.Length ? buffer.Length : rx.Length; - } + rx.CopyTo(buffer); + return rx.Length > buffer.Length ? buffer.Length : rx.Length; } return 0; @@ -117,7 +113,6 @@ public void Close() /// The received message. internal void Enqueue(ReadOnlySpan payload) { - this.messages.Enqueue(payload.ToArray()); - this.sema.Release(); + this.messages.Add(payload.ToArray()); } } diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 80c5d23..202eebe 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha4 + 0.6.3-alpha21 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -41,7 +41,7 @@ - + diff --git a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs index e86ff0f..865f712 100644 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs @@ -10,6 +10,7 @@ using Channel; using Configuration; using DTLS; + using LazyCache; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,6 +21,7 @@ using Org.BouncyCastle.Ocsp; using Org.BouncyCastle.Tls; using Org.BouncyCastle.X509; + using Threading; using WorldDirect.CoAP.Log; /// @@ -37,7 +39,6 @@ public static class ServiceProviderExtensions /// Configures a based on the provided configuration. /// /// - /// If DTLS is used for for encryption a must be provided in the service provider. /// If PSKs should be used must be added to the ServiceCollection. /// /// @@ -45,6 +46,7 @@ public static class ServiceProviderExtensions /// public static IServiceCollection ConfigureCoAPServer(this IServiceCollection services, IConfiguration configuration) { + services.AddLazyCache(); services.AddSingleton(serviceProvider => Configure(serviceProvider, configuration)); return services; @@ -97,7 +99,15 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura var channel = new UDPChannel(listenEndpoint.Endpoint); var config = (CoapConfig)CoapConfig.Default; - config.MaxMessageSize = options.MaxMessageSize; + // config.MaxMessageSize is used to check payload length + // we SHOULD store DTLSServer.MaxFragmentLength in block layer for each client + // to be able to determine how long the longest CoAP Message for a client is + // but this is too much of a refactoring at the moment + // thats why we use some buffer and only support FragmentLength defined by MaxMessageSize + // possible values are 128, 256 and 512 + // MaxMessageSize must be set to the least expected FragmentLength of the DTLS Clients + // Otherwise sending request wont work the FragmentLength is not in sync with the Blockwise layer + config.MaxMessageSize = options.MaxMessageSize - 64; if(config.MaxMessageSize <= config.DefaultBlockSize) { config.DefaultBlockSize = config.MaxMessageSize / 2; @@ -106,8 +116,8 @@ private static CoapServer Configure(IServiceProvider serviceProvider, IConfigura channel.SendBufferSize = config.ChannelSendBufferSize; channel.ReceivePacketSize = config.ChannelReceivePacketSize; - var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), dtlsServerBuilder.Config, channel, config); - + var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), dtlsServerBuilder.Config, channel, config); + //ep.Executor = new ThreadPoolExecutor(); server.AddEndPoint(ep); } } diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 874a556..f8d624d 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha4 + 0.6.3-alpha21 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -42,6 +42,7 @@ + @@ -49,7 +50,6 @@ - diff --git a/WorldDirect.CoAP/Channel/UDPChannel.cs b/WorldDirect.CoAP/Channel/UDPChannel.cs index 66eadfb..0c7aadf 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.cs @@ -240,6 +240,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); @@ -310,6 +311,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 4d25870..2208612 100644 --- a/WorldDirect.CoAP/CoapClient.cs +++ b/WorldDirect.CoAP/CoapClient.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Log; @@ -459,23 +460,50 @@ 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)); Action success = (r) => { + activity?.AddTag("coap.statuscode", r.StatusCode); + activity?.Stop(); tcs.TrySetResult(r); cancellation.Dispose(); }; @@ -486,11 +514,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."); } @@ -499,6 +529,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(); }; 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/Exchange.cs b/WorldDirect.CoAP/Net/Exchange.cs index 58c85c1..bd4b925 100644 --- a/WorldDirect.CoAP/Net/Exchange.cs +++ b/WorldDirect.CoAP/Net/Exchange.cs @@ -30,7 +30,6 @@ public class Exchange { private readonly ConcurrentDictionary _attributes = new ConcurrentDictionary(); private readonly Origin _origin; - private Activity activity; private Boolean _timedOut; private Request _request; private Request _currentRequest; @@ -53,52 +52,6 @@ public Exchange(Request request, Origin origin) _origin = origin; _currentRequest = request; _timestamp = DateTime.Now; - if (origin == Origin.Local) - { - this.activity = Tracing.ClientSource.StartActivity("CoAP Request"); - } - else - { - Activity.Current = null; - this.activity = Tracing.ServerSource.CreateActivity("CoAP Request", 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); - if (origin == Origin.Local) - { - this.activity?.AddTag("coap.remote", request.Destination); - } - - request.Retransmitting += (obj, ev) => this.activity?.AddEvent(new ActivityEvent("Retransmitting")); - request.Respond += (obj, ev) => - { - this.activity?.AddTag("coap.statuscode", ev.Response.StatusCode); - this.activity?.Stop(); - }; - request.TimedOut += (obj, ev) => - { - this.activity?.AddTag("coap.statuscode", "TIMEOUT"); - this.activity?.Stop(); - }; - request.Rejected += (obj, ev) => - { - this.activity?.AddTag("coap.statuscode", "REJECTED"); - this.activity?.Stop(); - }; - request.Responding += (obj, ev) => - { - var response = ev.Response; - if (response.Block1 != null) - { - this.activity?.AddEvent(new ActivityEvent($"New Block {ev.Response.Block1?.NUM}")); - } - else if (response.Block2 != null) - { - this.activity?.AddEvent(new ActivityEvent($"New Block {ev.Response.Block2?.NUM}")); - } - }; } public Origin Origin @@ -106,8 +59,6 @@ public Origin Origin get { return _origin; } } - public Activity Activity => this.activity; - /// /// Gets or sets the endpoint which has created and processed this exchange. /// @@ -217,8 +168,6 @@ public Boolean Complete if (value) { Completed?.Invoke(this, EventArgs.Empty); - this.Activity?.Stop(); - this.activity = null; } } } 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 b6ed997..20301d8 100644 --- a/WorldDirect.CoAP/Server/Resources/CoapExchange.cs +++ b/WorldDirect.CoAP/Server/Resources/CoapExchange.cs @@ -24,6 +24,7 @@ public class CoapExchange { readonly Exchange _exchange; readonly Resource _resource; + private Activity activity; private String _locationPath; private String _locationQuery; @@ -38,6 +39,14 @@ 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) @@ -45,8 +54,6 @@ public T Get(Object key) return this._exchange.Get(key); } - public Activity Activity => this._exchange.Activity; - /// /// Gets the request. /// @@ -183,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/ServerMessageDeliverer.cs b/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs index 86dd69f..45d45e0 100644 --- a/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs +++ b/WorldDirect.CoAP/Server/ServerMessageDeliverer.cs @@ -13,6 +13,7 @@ namespace WorldDirect.CoAP.Server { using System; using System.Collections.Generic; + using System.Diagnostics; using Log; using Microsoft.Extensions.Logging; using Net; @@ -45,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) { @@ -57,6 +61,7 @@ public void DeliverRequest(Exchange exchange) executor.Start(() => resource.HandleRequest(exchange)); else resource.HandleRequest(exchange); + } else { diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index f72560c..7b43d7e 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -1,8 +1,8 @@  - netstandard2.0 - 0.6.1-alpha1 + netstandard2.1 + 0.6.1-alpha21 World-Direct eBusiness solutions GmbH 2021 LICENSE From 54d655c94a2fe02b7163419d29958c53e89d218f Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Tue, 28 Nov 2023 10:30:16 +0100 Subject: [PATCH 18/27] fix message id reusage and better traceability of retransmission --- .../WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 2 +- WorldDirect.CoAP/Channel/UDPChannel.NET40.cs | 2 +- WorldDirect.CoAP/CoapClient.cs | 6 +- .../Deduplication/SweepDeduplicator.cs | 44 +++++++--- WorldDirect.CoAP/Message.cs | 2 +- WorldDirect.CoAP/Net/Exchange.cs | 9 +- WorldDirect.CoAP/Net/Matcher.cs | 46 +++++----- WorldDirect.CoAP/Net/MessageIdProvider.cs | 86 +++++++++++++++++++ WorldDirect.CoAP/Stack/ReliabilityLayer.cs | 25 ++++-- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 23 ++--- 11 files changed, 188 insertions(+), 59 deletions(-) create mode 100644 WorldDirect.CoAP/Net/MessageIdProvider.cs diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 202eebe..cbfd705 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha21 + 0.6.3-alpha30 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index f8d624d..7da2454 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha21 + 0.6.3-alpha30 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs index d389c60..ab90533 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs @@ -66,7 +66,7 @@ private void BeginSend(UDPSocket socket, Byte[] data, System.Net.EndPoint destin if (destination is IPEndPoint ep) { - log.LogDebug(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) diff --git a/WorldDirect.CoAP/CoapClient.cs b/WorldDirect.CoAP/CoapClient.cs index 2208612..e2c36b1 100644 --- a/WorldDirect.CoAP/CoapClient.cs +++ b/WorldDirect.CoAP/CoapClient.cs @@ -497,7 +497,11 @@ public Task SendAsync(Request request, CancellationToken ct) }; } TaskCompletionSource tcs = new TaskCompletionSource(); - var cancellation = ct.Register(() => tcs.TrySetCanceled(ct)); + var cancellation = ct.Register(() => + { + tcs.TrySetCanceled(ct); + request.Cancel(); + }); Action success = (r) => diff --git a/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs b/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs index dce0b56..a3c9282 100644 --- a/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs +++ b/WorldDirect.CoAP/Deduplication/SweepDeduplicator.cs @@ -14,6 +14,7 @@ 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; @@ -23,8 +24,9 @@ class SweepDeduplicator : IDeduplicator { 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; @@ -35,15 +37,21 @@ public SweepDeduplicator(ICoapConfig config) private void Sweep(object state) { - log.LogDebug("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) { - log.LogDebug("Mark-And-Sweep removes " + pair.Key); + log.LogTrace("Mark-And-Sweep removes " + pair.Key); keysToRemove.Add(pair.Key); } } @@ -52,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); } } } @@ -73,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; } @@ -92,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/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/Net/Exchange.cs b/WorldDirect.CoAP/Net/Exchange.cs index bd4b925..ed3d7eb 100644 --- a/WorldDirect.CoAP/Net/Exchange.cs +++ b/WorldDirect.CoAP/Net/Exchange.cs @@ -271,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/Matcher.cs b/WorldDirect.CoAP/Net/Matcher.cs index 56f17ac..fe55b9f 100644 --- a/WorldDirect.CoAP/Net/Matcher.cs +++ b/WorldDirect.CoAP/Net/Matcher.cs @@ -14,6 +14,7 @@ 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; @@ -40,14 +41,15 @@ public 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); } /// @@ -56,6 +58,7 @@ public void Start() if (System.Threading.Interlocked.CompareExchange(ref _running, 1, 0) > 0) return; _deduplicator.Start(); + this.currentIdProvider.Start(); } /// @@ -64,6 +67,7 @@ public void Stop() if (System.Threading.Interlocked.Exchange(ref _running, 0) == 0) return; _deduplicator.Stop(); + this.currentIdProvider.Stop(); Clear(); } @@ -80,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 @@ -90,12 +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; - log.LogDebug("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 @@ -194,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 @@ -212,7 +219,7 @@ public Exchange ReceiveRequest(Request request) } else { - log.LogTrace("Duplicate request: {Request}", request); + log.LogDebug("Duplicate request: {MessageId} from {Remote}", request.ID, request.Source); request.Duplicate = true; return previous; } @@ -284,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; @@ -302,13 +303,13 @@ public Exchange ReceiveResponse(Response response) if (prev != null) { // (and thus it holds: prev == exchange) - log.LogInformation("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); - log.LogDebug("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); } @@ -329,7 +330,7 @@ public Exchange ReceiveResponse(Response response) Exchange prev = _deduplicator.Find(keyId); if (prev != null) { - log.LogInformation("Duplicate response for completed exchange: " + response); + log.LogInformation("Duplicate response for completed exchange: " + response); response.Duplicate = true; return prev; } @@ -368,6 +369,7 @@ public void Dispose() IDisposable d = _deduplicator as IDisposable; if (d != null) d.Dispose(); + this.currentIdProvider.Dispose(); } private void RemoveNotificatoinsOf(ObserveRelation relation) @@ -394,10 +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); - log.LogDebug("Exchange completed: Cleaning up " + keyToken); + log.LogTrace("Exchange completed: Cleaning up " + keyToken); _exchangesByToken.Remove(keyToken); // in case an empty ACK was lost 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/Stack/ReliabilityLayer.cs b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs index 1306b5b..3d5cea2 100644 --- a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs +++ b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs @@ -12,6 +12,7 @@ namespace WorldDirect.CoAP.Stack { using System; + using System.Diagnostics; using System.Threading; using Log; using Microsoft.Extensions.Logging; @@ -46,7 +47,6 @@ public override void SendRequest(INextLayer nextLayer, Exchange exchange, Reques if (request.Type == MessageType.CON) { - log.LogTrace("Scheduling retransmission for " + request); PrepareRetransmission(exchange, request, ctx => SendRequest(nextLayer, exchange, request)); } @@ -230,8 +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; @@ -306,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, @@ -316,22 +325,22 @@ void timer_Elapsed(object state) if (_message.IsAcknowledged) { - log.LogTrace("Timeout: message already acknowledged, cancel retransmission of " + _message); + log.LogTrace("Timeout: message already acknowledged, cancel retransmission of " + _message); return; } else if (_message.IsRejected) { - log.LogTrace("Timeout: message already rejected, cancel retransmission of " + _message); + log.LogTrace("Timeout: message already rejected, cancel retransmission of " + _message); return; } else if (_message.IsCancelled) { - log.LogTrace("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)) { - log.LogTrace("Timeout: retransmit message, failed: " + failedCount + ", message: " + _message); + log.LogDebug("Timeout: retransmit message for the {retry}. time.", failedCount); _message.FireRetransmitting(); @@ -341,7 +350,7 @@ void timer_Elapsed(object state) } else { - log.LogTrace("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/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 7b43d7e..982c827 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,22 +2,23 @@ netstandard2.1 - 0.6.1-alpha21 + 0.6.1-alpha30 World-Direct eBusiness solutions GmbH 2021 LICENSE packageIcon.png https://github.com/world-direct/CoAP.NET - Changelog: - v0.6.1: add tracing diagnostics - 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 + + 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 From 01d3e98753fe30052430032a9d5423557db20acf Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Fri, 2 Feb 2024 13:59:52 +0100 Subject: [PATCH 19/27] better logging --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 19 ++++++++++++++++++- WorldDirect.CoAP.DTLS/DTLSSession.cs | 4 ++++ WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 10 ++++++++-- WorldDirect.CoAP/Tracing.cs | 6 ++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index d672241..ca3a6f5 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -13,6 +13,7 @@ namespace WorldDirect.CoAP.Net { using System; using System.Net; + using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Threading; using Channel; @@ -22,7 +23,6 @@ namespace WorldDirect.CoAP.Net using Log; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; - using Org.BouncyCastle.Tls; using Stack; using Threading; @@ -245,6 +245,13 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) 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) @@ -263,6 +270,12 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) 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) @@ -270,6 +283,10 @@ private void Channel_DataReceived(object? sender, DTLSDataReceivedEventArgs e) 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); diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 912771a..72273b1 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -104,6 +104,10 @@ public void Enqueue(ReadOnlySpan payload) } } } + 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) diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index ddc7e41..72804c1 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -52,7 +52,7 @@ public DTLSSessionManager(IAppCache cache, IUDPSender sender, DTLSServerConfig d public void SendTo(ReadOnlySpan packet, EndPoint endPoint) { // cache.TryGetValue does not work (always returns false with null object...) - var session = this.cache.Get(endPoint.ToString()); + var session = this.cache.Get(GetKey(endPoint)); if (session != null) { session.Send(packet); @@ -77,7 +77,7 @@ public void Stop() /// The endpoint who sent the packet. internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { - var session = this.cache.GetOrAdd(endPoint.ToString(), entry => + var session = this.cache.GetOrAdd(GetKey(endPoint), entry => { entry.SlidingExpiration = config.SessionTimeout; var callback = new PostEvictionCallbackRegistration() @@ -86,6 +86,7 @@ internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) 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; @@ -114,6 +115,11 @@ 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; diff --git a/WorldDirect.CoAP/Tracing.cs b/WorldDirect.CoAP/Tracing.cs index 97409a7..b97ad15 100644 --- a/WorldDirect.CoAP/Tracing.cs +++ b/WorldDirect.CoAP/Tracing.cs @@ -7,14 +7,16 @@ internal static class Tracing { + public static readonly string ClientActivityName = "WorldDirect.CoAP.Client"; + public static readonly string ServerActivityName = "WorldDirect.CoAP.Server"; /// /// The activity source for the tracing client events. /// - internal static readonly ActivitySource ClientSource = new ActivitySource("WorldDirect.CoAP.Client", "1.0.0"); + internal static readonly ActivitySource ClientSource = new ActivitySource(ClientActivityName, "1.0.0"); /// /// The activity source for the tracing events. /// - internal static readonly ActivitySource ServerSource = new ActivitySource("WorldDirect.CoAP.Server", "1.0.0"); + internal static readonly ActivitySource ServerSource = new ActivitySource(ServerActivityName, "1.0.0"); } } From 09d5c6931e9dcc8a18d2d7f46e3f02e86d03d48d Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 24 Apr 2024 16:09:11 +0200 Subject: [PATCH 20/27] fix insecure endpoint --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 2 +- WorldDirect.CoAP/Net/CoAPEndPoint.cs | 1 + WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index cbfd705..df4bade 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha30 + 0.6.3-alpha32 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index 7da2454..e417b11 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha30 + 0.6.3-alpha32 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP/Net/CoAPEndPoint.cs b/WorldDirect.CoAP/Net/CoAPEndPoint.cs index c62998b..d33ccfc 100644 --- a/WorldDirect.CoAP/Net/CoAPEndPoint.cs +++ b/WorldDirect.CoAP/Net/CoAPEndPoint.cs @@ -240,6 +240,7 @@ private void ReceiveData(DataReceivedEventArgs e) try { request = decoder.DecodeRequest(); + request.EndPoint = this; } catch (Exception) { diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 982c827..9cc645f 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,7 +2,7 @@ netstandard2.1 - 0.6.1-alpha30 + 0.6.1-alpha32 World-Direct eBusiness solutions GmbH 2021 LICENSE From f9302051ef7d03939f47a6f07bd4e91a06071acd Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Mon, 6 May 2024 14:44:59 +0200 Subject: [PATCH 21/27] forward block retries to originally request --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Server.Extensions.csproj | 2 +- WorldDirect.CoAP/Stack/BlockwiseLayer.cs | 4 +++- WorldDirect.CoAP/Stack/ReliabilityLayer.cs | 2 +- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index df4bade..7f489cc 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha32 + 0.6.3-alpha33 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj index e417b11..3b317a9 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha32 + 0.6.3-alpha33 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs index 564189f..a5b3f89 100644 --- a/WorldDirect.CoAP/Stack/BlockwiseLayer.cs +++ b/WorldDirect.CoAP/Stack/BlockwiseLayer.cs @@ -34,7 +34,7 @@ public BlockwiseLayer(ICoapConfig config) _maxMessageSize = config.MaxMessageSize; _defaultBlockSize = config.DefaultBlockSize; _blockTimeout = config.BlockwiseStatusLifetime; - log.LogDebug("BlockwiseLayer uses MaxMessageSize: " + _maxMessageSize + " and DefaultBlockSize:" + _defaultBlockSize); + log.LogInformation("BlockwiseLayer uses MaxMessageSize: " + _maxMessageSize + " and DefaultBlockSize:" + _defaultBlockSize); config.PropertyChanged += ConfigChanged; } @@ -496,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; } diff --git a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs index 3d5cea2..35bf04e 100644 --- a/WorldDirect.CoAP/Stack/ReliabilityLayer.cs +++ b/WorldDirect.CoAP/Stack/ReliabilityLayer.cs @@ -230,7 +230,7 @@ private void PrepareRetransmission(Exchange exchange, Message msg, Action netstandard2.1 - 0.6.1-alpha32 + 0.6.1-alpha33 World-Direct eBusiness solutions GmbH 2021 LICENSE From 8b468fb6d2ec974a49b2d5dcf7cc63927a2d0f2b Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 15 May 2024 14:15:44 +0200 Subject: [PATCH 22/27] adapt configuration and DI on Hostbuilder --- EnergySoultions.CoAP.sln | 4 +- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 19 +- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 5 +- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 7 +- .../WorldDirect.CoAP.DTLS.csproj | 4 +- .../X509CertificateExtensions.cs | 24 ++ .../Configuration/BindingAddress.cs | 22 +- .../Configuration/CertificateLoader.cs | 235 +++++++++++++++ .../Configuration}/CertificateManager.cs | 4 +- .../Configuration/CertificateOption.cs | 17 ++ .../CryptographyExtensions.cs | 19 ++ .../CipherSuiteConfigurationCallback.cs | 7 + .../Hosting/CoAPEndpointBuilder.cs | 26 ++ .../Hosting/CoAPEndpointOptions.cs | 12 + .../Hosting/CoAPOptions.cs | 17 ++ .../Hosting/CoAPSEndpointBuilder.cs | 26 ++ .../Hosting/CoAPSEndpointBuilderExtensions.cs | 51 ++++ .../Hosting/CoAPSEndpointOptions.cs | 29 ++ .../Hosting/CoAPServerBuilder.cs | 21 ++ .../Hosting/CoAPServerBuilderExtensions.cs | 136 +++++++++ .../Hosting/EndpointSpecific.cs | 25 ++ .../Hosting/HostBuilderExtensions.cs | 26 ++ .../Hosting/ICoAPEndpointBuilder.cs | 19 ++ .../Hosting/ICoAPSEndpointBuilder.cs | 8 + .../Hosting/ICoAPServerBuilder.cs | 14 + .../Hosting/IEndpointSpecific.cs | 18 ++ .../Hosting/ServiceCollectionExtensions.cs | 63 ++++ .../Hosting/Services/CoAPServerService.cs | 51 ++++ .../Services/VitalBackgroundService.cs | 141 +++++++++ .../Hosting/TraceProviderBuilderExtensions.cs | 21 ++ .../InMemoryPasswordFinder.cs | 13 +- .../README.md | 0 .../WorldDirect.CoAP.Hosting.csproj | 9 +- .../Configuration/BindingAddressSpecs.cs | 65 ---- .../Configuration/CertificationConfigSpecs.cs | 174 ----------- .../Configuration/ConfigurationReaderSpecs.cs | 86 ------ ...Direct.CoAP.Server.Extensions.Specs.csproj | 35 --- .../Configuration/CertificateConfig.cs | 33 -- .../Configuration/CoAPServerOptions.cs | 17 -- .../Configuration/CoAPServerOptionsLoader.cs | 50 --- .../Configuration/ConfigurationReader.cs | 67 ---- .../Configuration/EndpointConfig.cs | 28 -- .../Configuration/ListenOption.cs | 15 - .../CryptographyExtensions.cs | 11 - .../DTLSServerConfigBuilder.cs | 285 ------------------ .../ServiceProviderExtensions.cs | 130 -------- WorldDirect.CoAP/Tracing.cs | 9 +- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 48 files changed, 1075 insertions(+), 1025 deletions(-) create mode 100644 WorldDirect.CoAP.DTLS/X509CertificateExtensions.cs rename {WorldDirect.CoAP.Server.Extensions => WorldDirect.CoAP.Hosting}/Configuration/BindingAddress.cs (83%) create mode 100644 WorldDirect.CoAP.Hosting/Configuration/CertificateLoader.cs rename {WorldDirect.CoAP.Server.Extensions => WorldDirect.CoAP.Hosting/Configuration}/CertificateManager.cs (99%) create mode 100644 WorldDirect.CoAP.Hosting/Configuration/CertificateOption.cs create mode 100644 WorldDirect.CoAP.Hosting/CryptographyExtensions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CipherSuiteConfigurationCallback.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPEndpointOptions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPOptions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointBuilderExtensions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPSEndpointOptions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/CoAPServerBuilderExtensions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/EndpointSpecific.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/HostBuilderExtensions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/ICoAPEndpointBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/ICoAPSEndpointBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/ICoAPServerBuilder.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/IEndpointSpecific.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/ServiceCollectionExtensions.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/Services/CoAPServerService.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/Services/VitalBackgroundService.cs create mode 100644 WorldDirect.CoAP.Hosting/Hosting/TraceProviderBuilderExtensions.cs rename {WorldDirect.CoAP.Server.Extensions => WorldDirect.CoAP.Hosting}/InMemoryPasswordFinder.cs (50%) rename {WorldDirect.CoAP.Server.Extensions => WorldDirect.CoAP.Hosting}/README.md (100%) rename WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj => WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj (87%) delete mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs delete mode 100644 WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs diff --git a/EnergySoultions.CoAP.sln b/EnergySoultions.CoAP.sln index c06f89e..532ebd0 100644 --- a/EnergySoultions.CoAP.sln +++ b/EnergySoultions.CoAP.sln @@ -23,9 +23,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Example.Se 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.Server.Extensions", "WorldDirect.CoAP.Server.Extensions\WorldDirect.CoAP.Server.Extensions.csproj", "{7D124DB3-E5FF-40BC-BB80-213D5BB87F2F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorldDirect.CoAP.Server.Extensions.Specs", "WorldDirect.CoAP.Server.Extensions.Specs\WorldDirect.CoAP.Server.Extensions.Specs.csproj", "{C3D5BDEA-1B88-42F9-8C4B-61FCF0916808}" +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 diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index ca3a6f5..569d41d 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -19,7 +19,6 @@ namespace WorldDirect.CoAP.Net using Channel; using Codec; using DTLS; - using LazyCache; using Log; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; @@ -61,7 +60,7 @@ public partial class CoAPSEndpoint : IEndPoint, IOutbox /// Instantiates a new endpoint with the /// specified channel and configuration. /// - public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfig config) { _config = config; _matcher = new Matcher(this._config); @@ -75,7 +74,7 @@ public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, ICoapConfig c this.DTLSConfig = dtlsConfig; } - public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, UDPChannel channel, ICoapConfig config) + public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, UDPChannel channel, ICoapConfig config) { _config = config; _matcher = new Matcher(this._config); @@ -85,6 +84,20 @@ public CoAPSEndpoint(IAppCache cache, DTLSServerConfig dtlsConfig, UDPChannel ch 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); + + // DTLS Header has 9 bytes + udpChannel.ReceivePacketSize = config.MaxMessageSize + 9; + this.channel = new DTLSChannel(udpChannel, cache, dtlsConfig); + this.channel.DtlsDataReceived += Channel_DataReceived; + this.DTLSConfig = dtlsConfig; + } + public DTLSServerConfig DTLSConfig { get; } diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index 6d5a4ae..6b27b71 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; - using LazyCache; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using WorldDirect.CoAP.Log; @@ -28,7 +27,7 @@ public class DTLSChannel : IChannel /// The cache to store dtls sessions. /// The configuration of the dtls server. /// The timeout after which a session is deleted. - public DTLSChannel(UDPChannel channel, IAppCache cache, DTLSServerConfig dtlsConfig, TimeSpan sessionTimeout) + public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtlsConfig, TimeSpan sessionTimeout) { this.channel = channel; this.channel.DataReceived += DtlsReceived; @@ -42,7 +41,7 @@ public DTLSChannel(UDPChannel channel, IAppCache cache, DTLSServerConfig dtlsCon /// 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, IAppCache cache, DTLSServerConfig dtlsConfig) + public DTLSChannel(UDPChannel channel, IMemoryCache cache, DTLSServerConfig dtlsConfig) : this(channel, cache, dtlsConfig, TimeSpan.FromMinutes(2)) { } diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 72804c1..8f96aff 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -4,7 +4,6 @@ using System.Net; using System.Text; using Channel; - using LazyCache; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -18,7 +17,7 @@ /// public class DTLSSessionManager { - private readonly IAppCache cache; + private readonly IMemoryCache cache; private readonly IUDPSender sender; private readonly DTLSServerConfig dtlsServerConfig; private readonly DTLSSessionConfig config; @@ -31,7 +30,7 @@ public class DTLSSessionManager /// An object to send udp packets. /// The configuration of the dtls server. /// The configuration for the sessions. - public DTLSSessionManager(IAppCache cache, IUDPSender sender, DTLSServerConfig dtlsServerConfig, DTLSSessionConfig config) + public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfig dtlsServerConfig, DTLSSessionConfig config) { this.cache = cache; this.sender = sender; @@ -77,7 +76,7 @@ public void Stop() /// The endpoint who sent the packet. internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { - var session = this.cache.GetOrAdd(GetKey(endPoint), entry => + var session = this.cache.GetOrCreate(GetKey(endPoint), entry => { entry.SlidingExpiration = config.SessionTimeout; var callback = new PostEvictionCallbackRegistration() diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index 7f489cc..f49c2c0 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha33 + 0.6.3-alpha34 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -41,7 +41,7 @@ - + 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.Server.Extensions/Configuration/BindingAddress.cs b/WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs similarity index 83% rename from WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs rename to WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs index 55bcf86..f01639b 100644 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/BindingAddress.cs +++ b/WorldDirect.CoAP.Hosting/Configuration/BindingAddress.cs @@ -1,6 +1,7 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration; +namespace WorldDirect.CoAP.Hosting.Configuration; using System.Globalization; +using System.Net; /// /// An address a CoAP server may bind to. @@ -19,6 +20,23 @@ private BindingAddress(string scheme, string host, int port) 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 @@ -84,4 +102,4 @@ public static BindingAddress Parse(string address) return new BindingAddress(host: host, port: port, scheme: scheme); } -} \ No newline at end of file +} 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.Server.Extensions/CertificateManager.cs b/WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs similarity index 99% rename from WorldDirect.CoAP.Server.Extensions/CertificateManager.cs rename to WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs index ba6d233..725aeb7 100644 --- a/WorldDirect.CoAP.Server.Extensions/CertificateManager.cs +++ b/WorldDirect.CoAP.Hosting/Configuration/CertificateManager.cs @@ -1,4 +1,4 @@ -namespace WorldDirect.CoAP.Server.Extensions; +namespace WorldDirect.CoAP.Hosting.Configuration; using System.Security.Cryptography.X509Certificates; @@ -147,4 +147,4 @@ private static bool IsCertificateAllowedForServerAuth(X509Certificate2 certifica return !hasEkuExtension; } -} \ No newline at end of file +} 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.Server.Extensions/InMemoryPasswordFinder.cs b/WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs similarity index 50% rename from WorldDirect.CoAP.Server.Extensions/InMemoryPasswordFinder.cs rename to WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs index c9ac877..802e8b9 100644 --- a/WorldDirect.CoAP.Server.Extensions/InMemoryPasswordFinder.cs +++ b/WorldDirect.CoAP.Hosting/InMemoryPasswordFinder.cs @@ -1,16 +1,25 @@ -namespace WorldDirect.CoAP.Server.Extensions; +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(); } -} \ No newline at end of file +} diff --git a/WorldDirect.CoAP.Server.Extensions/README.md b/WorldDirect.CoAP.Hosting/README.md similarity index 100% rename from WorldDirect.CoAP.Server.Extensions/README.md rename to WorldDirect.CoAP.Hosting/README.md diff --git a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj similarity index 87% rename from WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj rename to WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj index 3b317a9..5e81ad9 100644 --- a/WorldDirect.CoAP.Server.Extensions/WorldDirect.CoAP.Server.Extensions.csproj +++ b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha33 + 0.6.3-alpha34 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -42,14 +42,19 @@ - + + + + + + diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs deleted file mode 100644 index 987b364..0000000 --- a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/BindingAddressSpecs.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration -{ - using FluentAssertions; - using WorldDirect.CoAP.Server.Extensions.Configuration; - using Xunit; - - public class BindingAddressSpecs - { - - - private readonly int validPort = 5684; - private readonly string coapScheme = "coap"; - private readonly string coapsScheme = "coaps"; - private readonly string validBindingAddress; - public BindingAddressSpecs() - { - this.validBindingAddress = $"{coapScheme}://*:{validPort}/"; - } - - [Fact] - public void RecognizesSchemeCorrectly() - { - var bindingAddress = BindingAddress.Parse(this.validBindingAddress); - bindingAddress.Scheme.Should().Be(this.coapScheme); - } - - [Fact] - public void RecognizesPortCorrectly() - { - var bindingAddress = BindingAddress.Parse(this.validBindingAddress); - bindingAddress.Port.Should().Be(this.validPort); - } - - [Fact] - public void RecognizesHostCorrectly() - { - var bindingAddress = BindingAddress.Parse(this.validBindingAddress); - bindingAddress.Host.Should().Be("*"); - } - - [Fact] - public void RecognizesLocalhostCorrectly() - { - var address = "coap://localhost:1234/"; - var bindingAddress = BindingAddress.Parse(address); - bindingAddress.Host.Should().Be("localhost"); - } - - [Fact] - public void RecognizesCoAPPortCorrectly() - { - var address = "coap://localhost"; - var bindingAddress = BindingAddress.Parse(address); - bindingAddress.Port.Should().Be(5683); - } - - [Fact] - public void RecognizesCoAPSPortCorrectly() - { - var address = "coaps://localhost"; - var bindingAddress = BindingAddress.Parse(address); - bindingAddress.Port.Should().Be(5684); - } - } -} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs deleted file mode 100644 index 0eaa1b3..0000000 --- a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/CertificationConfigSpecs.cs +++ /dev/null @@ -1,174 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration -{ - using System.Collections.Generic; - using FluentAssertions; - using Microsoft.Extensions.Configuration; - using WorldDirect.CoAP.Server.Extensions.Configuration; - using Xunit; - - public class CertificationConfigSpecs - { - private readonly string pathConfigValue = "path123"; - private readonly string keyPathConfigValue = "keyPath123"; - private readonly string passwordConfigValue = "password123"; - private readonly string subjectConfigValue = "subject123"; - private readonly string storeConfigValue = "store123"; - private readonly string locationConfigValue = "location123"; - private readonly IConfiguration exampleConfiguration; - - private CertificateConfig certificateConfig; - - public CertificationConfigSpecs() - { - var exampleDict = new Dictionary() - { - { "Path", pathConfigValue }, - { "KeyPath", keyPathConfigValue }, - { "Password", passwordConfigValue}, - { "Subject", subjectConfigValue }, - { "Store", storeConfigValue }, - { "Location", locationConfigValue}, - { "AllowInvalid", "true" } - }; - this.exampleConfiguration = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(this.exampleConfiguration); - } - - [Fact] - public void AssignConfigurationCorrectly() - { - this.certificateConfig.Configuration.Should().Be(this.exampleConfiguration); - } - - [Fact] - public void ExtractsPathCorrectly() - { - this.certificateConfig.Path.Should().Be(this.pathConfigValue); - } - - [Fact] - public void ExtractsKeyPathCorrectly() - { - this.certificateConfig.KeyPath.Should().Be(this.keyPathConfigValue); - } - - [Fact] - public void ExtractsPasswordCorrectly() - { - this.certificateConfig.Password.Should().Be(this.passwordConfigValue); - } - - [Fact] - public void ExtractsSubjectCorrectly() - { - this.certificateConfig.Subject.Should().Be(this.subjectConfigValue); - } - - [Fact] - public void ExtractsStoreCorrectly() - { - this.certificateConfig.Store.Should().Be(this.storeConfigValue); - } - - [Fact] - public void ExtractsLocationCorrectly() - { - this.certificateConfig.Location.Should().Be(this.locationConfigValue); - } - - [Fact] - public void ExtractsAllowInvalidCorrectly() - { - this.certificateConfig.AllowInvalid.Should().BeTrue(); - } - - [Fact] - public void CertificateIsFileWhenPathIsSet() - { - var exampleDict = new Dictionary() - { - { "Path", pathConfigValue }, - }; - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.IsFile.Should().BeTrue(); - } - - [Fact] - public void CertificateIsNotFileWhenPathIsNotSet() - { - var exampleDict = new Dictionary(); - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.IsFile.Should().BeFalse(); - } - - [Fact] - public void CertificateIsFromStoreWhenSubjectIsSet() - { - var exampleDict = new Dictionary() - { - { "Subject", subjectConfigValue }, - }; - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.IsFromStore.Should().BeTrue(); - } - - [Fact] - public void LocationDefaultsToCurrentUser() - { - var exampleDict = new Dictionary(); - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.Location.Should().Be("CurrentUser"); - } - - [Fact] - public void AllowInvalidIsFalsePerDefault() - { - var exampleDict = new Dictionary() - { - }; - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.AllowInvalid.Should().BeFalse(); - } - - [Fact] - public void CertificateIsFromStoreWhenSubjectIsNotSet() - { - var exampleDict = new Dictionary(); - var config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - - this.certificateConfig = new CertificateConfig(config); - - this.certificateConfig.IsFromStore.Should().BeFalse(); - } - } -} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs b/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs deleted file mode 100644 index 8ca4b25..0000000 --- a/WorldDirect.CoAP.Server.Extensions.Specs/Configuration/ConfigurationReaderSpecs.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Specs.Configuration -{ - using System; - using System.Collections.Generic; - using System.Linq; - using FluentAssertions; - using Microsoft.Extensions.Configuration; - using WorldDirect.CoAP.Server.Extensions.Configuration; - using Xunit; - - public class ConfigurationReaderSpecs - { - private readonly Dictionary exampleDict; - private readonly string exampleUrl = "coaps://localhost:5684"; - private readonly string examplePath = "example.pfx"; - private readonly string exampleName = "CoAPSWithCertAuth"; - private readonly TimeSpan exampleTimeout = TimeSpan.FromSeconds(20); - private readonly string exampleBaseKey; - private IConfiguration config; - private ConfigurationReader reader; - public ConfigurationReaderSpecs() - { - this.exampleBaseKey = $"Endpoints:{this.exampleName}"; - this.exampleDict = new Dictionary() - { - { $"{exampleBaseKey}:Url", this.exampleUrl}, - { $"{exampleBaseKey}:Certificate:Path", this.examplePath}, - { $"{exampleBaseKey}:ClientCA:0:Path", this.examplePath}, - { $"{exampleBaseKey}:ClientCA:1:Path", this.examplePath}, - { $"{exampleBaseKey}:HandshakeTimeout", this.exampleTimeout.ToString()}, - - }; - this.config = new ConfigurationBuilder() - .AddInMemoryCollection(exampleDict) - .Build(); - this.reader = new ConfigurationReader(this.config); - } - - [Fact] - public void ReadsOneEndpoint() - { - var endpoints = this.reader.Endpoints; - endpoints.Should().HaveCount(1); - } - - [Fact] - public void ReadsNameCorrectly() - { - var endpoints = this.reader.Endpoints; - var endpoint = endpoints.Single(); - endpoint.Name.Should().Be(this.exampleName); - } - - [Fact] - public void ReadsUrlCorrectly() - { - var endpoints = this.reader.Endpoints; - var endpoint = endpoints.Single(); - endpoint.Url.Should().Be(this.exampleUrl); - } - - [Fact] - public void ReadsTwoClientCAs() - { - var endpoints = this.reader.Endpoints; - var endpoint = endpoints.Single(); - endpoint.ClientCAs.Should().HaveCount(2); - } - - [Fact] - public void ReadsCertificateConfig() - { - var endpoints = this.reader.Endpoints; - var endpoint = endpoints.Single(); - endpoint.CertificateConfig.Should().NotBeNull(); - } - - [Fact] - public void ReadsHandshakeTimeoutCorrectly() - { - var endpoints = this.reader.Endpoints; - var endpoint = endpoints.Single(); - endpoint.HandshakeTimeout.Should().Be(exampleTimeout); - } - } -} diff --git a/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj b/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj deleted file mode 100644 index 7379746..0000000 --- a/WorldDirect.CoAP.Server.Extensions.Specs/WorldDirect.CoAP.Server.Extensions.Specs.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net6.0 - enable - - false - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - Always - - - - diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs deleted file mode 100644 index 817d5c3..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CertificateConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration -{ - using Microsoft.Extensions.Configuration; - - public class CertificateConfig - { - public CertificateConfig(IConfiguration configuration) - { - this.Configuration = configuration; - this.Path = configuration[nameof(this.Path)]; - this.KeyPath = configuration[nameof(this.KeyPath)]; - this.Password = configuration[nameof(this.Password)]; - this.Subject = configuration[nameof(this.Subject)]; - this.Store = configuration[nameof(this.Store)]; - this.Location = configuration[nameof(this.Location)] == null ? "CurrentUser" : configuration[nameof(this.Location)]; - this.AllowInvalid = configuration[nameof(this.AllowInvalid)] != null && bool.Parse(configuration[nameof(this.AllowInvalid)]); - } - - public IConfiguration Configuration { get; } - - 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; } - public bool AllowInvalid { get; set; } - } -} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs deleted file mode 100644 index e76599e..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration -{ - using System.Collections.Generic; - - public class CoAPServerOptions - { - - internal CoAPServerOptions(IEnumerable listenOptions) - { - this.ListenOptions = listenOptions; - } - - public IEnumerable ListenOptions { get; } - - public int MaxMessageSize { get; set; } = 1024; - } -} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs deleted file mode 100644 index 49c8f8d..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/CoAPServerOptionsLoader.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration; - -using System.Net; -using Microsoft.Extensions.Configuration; - -public class CoAPServerOptionsLoader -{ - private readonly IConfiguration config; - - public CoAPServerOptionsLoader(IConfiguration config) - { - this.config = config; - } - - public CoAPServerOptions Options => this.Build(); - - private CoAPServerOptions Build() - { - var reader = new ConfigurationReader(this.config); - var endpoints = reader.Endpoints; - - var listenOptions = new List(); - - foreach (var endpoint in endpoints) - { - var address = BindingAddress.Parse(endpoint.Url); - if (address.Host == "localhost") - { - listenOptions.Add(new ListenOption(new IPEndPoint(IPAddress.Loopback, address.Port), endpoint)); - } - else - { - var ipAddress = IPAddress.Any; - if(IPAddress.TryParse(address.Host, out var parsedAddress)) - { - ipAddress = parsedAddress; - } - listenOptions.Add(new ListenOption(new IPEndPoint(ipAddress, address.Port), endpoint)); - } - } - - var options = new CoAPServerOptions(listenOptions); - if(reader.MaxMessageSize.HasValue) - { - options.MaxMessageSize = reader.MaxMessageSize.Value; - } - - return options; - } -} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs deleted file mode 100644 index 74e230a..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/ConfigurationReader.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration; - -using Microsoft.Extensions.Configuration; - -/// -/// -/// -/// -/// Based on Kestrel: -/// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L67 -/// -public class ConfigurationReader -{ - private const string EndpointsKey = "Endpoints"; - private const string UrlKey = "Url"; - private const string CertificateKey = "Certificate"; - private const string ClientCAKey = "ClientCA"; - private const string HandshakeTimeoutKey = "HandshakeTimeout"; - private const string MaxMessageSizeKey = "MaxMessageSize"; - private readonly IConfiguration config; - - public ConfigurationReader(IConfiguration config) - { - this.config = config; - } - - public IEnumerable Endpoints => this.ReadEndpoints(); - - public int? MaxMessageSize => this.config.GetSection(MaxMessageSizeKey).Exists() ? this.config.GetSection(MaxMessageSizeKey).Get() : null; - - private IEnumerable ReadEndpoints() - { - var endpoints = new List(); - var endpointConfig = this.config.GetSection(EndpointsKey); - var endpointsConfigurations = endpointConfig.GetChildren(); - foreach (var endpointCfg in endpointsConfigurations) - { - var url = endpointCfg[UrlKey]; - if (string.IsNullOrEmpty(url)) - { - throw new InvalidOperationException($"Url of endpoint {endpointCfg.Key} must be defined."); - } - - - CertificateConfig? certificateConfig = null; - if (endpointCfg.GetSection(CertificateKey).GetChildren().Any()) - { - certificateConfig = new CertificateConfig(endpointCfg.GetSection(CertificateKey)); - } - IEnumerable? clientCAConfig = null; - if (endpointCfg.GetSection(ClientCAKey).GetChildren().Any()) - { - clientCAConfig = endpointCfg.GetSection(ClientCAKey).GetChildren().Select(c => new CertificateConfig(c)); - } - var endpoint = new EndpointConfig(endpointCfg.Key, url) - { - CertificateConfig = certificateConfig, - ClientCAs = clientCAConfig != null ? clientCAConfig.ToList() : new List(), - HandshakeTimeout = endpointCfg.GetSection(HandshakeTimeoutKey).Get(), - }; - - endpoints.Add(endpoint); - } - - return endpoints; - } -} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs deleted file mode 100644 index c86c991..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/EndpointConfig.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration -{ - using System; - - /// - /// The conf - /// - /// - /// Based on Kestrel: - /// https://github.com/dotnet/aspnetcore/blob/68ae6b0d8aa2f4a0ff189d5cedc741e32cc643d2/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs#L267 - /// - public class EndpointConfig - { - - public EndpointConfig(string name, string url) - { - this.Name = name; - this.Url = url; - this.ClientCAs = new List(); - } - - public string Name { get; set; } - public string Url { get; set; } - public CertificateConfig? CertificateConfig { get; set; } - public List ClientCAs { get; set; } - public TimeSpan HandshakeTimeout { get; set; } - } -} diff --git a/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs b/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs deleted file mode 100644 index 43cfc9d..0000000 --- a/WorldDirect.CoAP.Server.Extensions/Configuration/ListenOption.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions.Configuration; - -using System.Net; - -public class ListenOption -{ - - public ListenOption(EndPoint endpoint, EndpointConfig endpointConfig) - { - this.Endpoint = endpoint; - this.EndpointConfig = endpointConfig; - } - public EndPoint Endpoint { get; set; } - public EndpointConfig EndpointConfig { get; set; } -} diff --git a/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs b/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs deleted file mode 100644 index e2d3320..0000000 --- a/WorldDirect.CoAP.Server.Extensions/CryptographyExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions; - -using System.Security.Cryptography.X509Certificates; - -public static class CryptographyExtensions -{ - public static Org.BouncyCastle.X509.X509Certificate ToBouncyCastle(this X509Certificate2 cert) - { - return new Org.BouncyCastle.X509.X509Certificate(cert.Export(X509ContentType.Cert)); - } -} \ No newline at end of file diff --git a/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs b/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs deleted file mode 100644 index 68b6c7d..0000000 --- a/WorldDirect.CoAP.Server.Extensions/DTLSServerConfigBuilder.cs +++ /dev/null @@ -1,285 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions; - -using System.Security.Cryptography.X509Certificates; -using Configuration; -using DTLS; -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.Impl.BC; - -public class DTLSServerConfigBuilder -{ - // TODO: Check if certificate usage is allowed for server auth when loaded from files - // TODO: Check if CA is allowed to be used for (KeyCertSign) when loaded from file - private readonly BcTlsCrypto crypto; - private readonly MutableDTLSServerConfig config; - public DTLSServerConfigBuilder() - { - this.crypto = new BcTlsCrypto(new SecureRandom()); - this.config = new MutableDTLSServerConfig(); - this.config.Crypto = this.crypto; - } - - public DTLSServerConfig Config => new (this.config); - - /// - /// Loads the servers certificate based on the configuration settings. - /// - /// The settings to identify the certificate. - /// - /// - public DTLSServerConfigBuilder SetCertificate(CertificateConfig config) - { - if (config.IsFromStore) - { - var ecdasCert = this.LoadCertAndKeyFromStore(config); - this.config.EcCertificate = ecdasCert; - } - else if (config.IsFile) - { - try - { - // *.pem and *.key file - if (!string.IsNullOrEmpty(config.Path) && !string.IsNullOrEmpty(config.KeyPath)) - { - var ecdsaCert = this.LoadCertAndKeyFromFiles(config); - this.config.EcCertificate = ecdsaCert; - } - // pfx file - else if (!string.IsNullOrEmpty(config.Path)) - { - var ecdsaCert = this.LoadCertAndKeyFromPfxFile(config); - this.config.EcCertificate = ecdsaCert; - } - else - { - throw new InvalidOperationException("Invalid configuration for certificate"); - } - } - catch (IOException e) - { - throw new InvalidOperationException($"Failed to load certificate file {config.Path}", e); - } - } - else - { - throw new InvalidOperationException($"Invalid configuration for certificate"); - } - this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8); - this.config.CipherSuites.Add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256); - - return this; - } - - public DTLSServerConfigBuilder AddCA(CertificateConfig config) - { - if (config.IsFromStore) - { - var cert = this.LoadCAFromStore(config); - this.config.CAs.Add(cert); - } - else if (config.IsFile) - { - try - { - // pfx file, password can be empty - if (!string.IsNullOrEmpty(config.Path) && config.Password != null) - { - throw new NotImplementedException(""); - } - // pem file - else if (!string.IsNullOrEmpty(config.Path)) - { - var cert = this.LoadCertFromFile(config.Path!); - this.config.CAs.Add(cert); - } - else - { - throw new InvalidOperationException("Invalid file configuration for CA certificate."); - } - } - catch (IOException e) - { - throw new InvalidOperationException($"Failed to load CA certificate file {config.Path}", e); - } - } - else - { - throw new InvalidOperationException($"Could not identify where to search for CA certificate."); - } - return this; - } - - public DTLSServerConfigBuilder SetHandshakeTimeout(TimeSpan timeout) - { - this.config.HandshakeTimeout = timeout; - return this; - } - - /// - /// Add an exporter of the session keys. - /// - /// ATTENTION!! will export session keys. Only use in development. - /// The store where the keys should be exported. - /// The builder. - public DTLSServerConfigBuilder EnableExportOfSessionKeys(IKeyStore store) - { - this.config.KeyStore = store; - return this; - } - - public DTLSServerConfigBuilder SetPskManager(TlsPskIdentityManager manager) - { - this.config.PskManager = manager; - this.config.CipherSuites.Add(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8); - this.config.CipherSuites.Add(CipherSuite.TLS_PSK_WITH_AES_128_CBC_SHA256); - return this; - } - - 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(CertificateConfig 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(CertificateConfig 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(CertificateConfig 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(CertificateConfig 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.Server.Extensions/ServiceProviderExtensions.cs b/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs deleted file mode 100644 index 865f712..0000000 --- a/WorldDirect.CoAP.Server.Extensions/ServiceProviderExtensions.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace WorldDirect.CoAP.Server.Extensions -{ - using System; - using System.Collections.Generic; - using System.Net; - using System.Runtime.CompilerServices; - using System.Security.Cryptography; - using System.Text; - using System.Threading.Tasks; - using Channel; - using Configuration; - using DTLS; - using LazyCache; - using Microsoft.Extensions.Caching.Memory; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Net; - using Org.BouncyCastle.Asn1.Cmp; - using Org.BouncyCastle.Asn1.X509; - using Org.BouncyCastle.Crypto; - using Org.BouncyCastle.Ocsp; - using Org.BouncyCastle.Tls; - using Org.BouncyCastle.X509; - using Threading; - using WorldDirect.CoAP.Log; - - /// - /// A helper function to determinate how PSKs are loaded and mapped to a CoAPS endpoint. - /// - /// The service provider to load needed services from. - /// The name of the endpoint configuration. - /// The psk store. - public delegate TlsPskIdentityManager? PskIdentityManagerResolver(IServiceProvider serviceProvider, string key); - - public static class ServiceProviderExtensions - { - - /// - /// Configures a based on the provided configuration. - /// - /// - /// If PSKs should be used must be added to the ServiceCollection. - /// - /// - /// - /// - public static IServiceCollection ConfigureCoAPServer(this IServiceCollection services, IConfiguration configuration) - { - services.AddLazyCache(); - services.AddSingleton(serviceProvider => Configure(serviceProvider, configuration)); - - return services; - } - - private static CoapServer Configure(IServiceProvider serviceProvider, IConfiguration configuration) - { - LogManager.Provider = serviceProvider; - var server = new CoapServer(); - - - var loader = new CoAPServerOptionsLoader(configuration); - var options = loader.Options; - - foreach (var listenEndpoint in options.ListenOptions) - { - if (listenEndpoint!.EndpointConfig.CertificateConfig == null && !listenEndpoint.EndpointConfig.Url.StartsWith("coaps")) - { - // insecure - server.AddEndPoint(listenEndpoint.Endpoint as IPEndPoint); - } - else - { - var dtlsServerBuilder = new DTLSServerConfigBuilder(); - if (listenEndpoint.EndpointConfig.CertificateConfig != null) - { - dtlsServerBuilder.SetCertificate(listenEndpoint.EndpointConfig.CertificateConfig); - } - dtlsServerBuilder.SetHandshakeTimeout(listenEndpoint.EndpointConfig.HandshakeTimeout); - var resolver = serviceProvider.GetService(); - if (resolver != null) - { - var pskManager = resolver(serviceProvider, listenEndpoint.EndpointConfig.Name); - if (pskManager != null) - { - dtlsServerBuilder.SetPskManager(pskManager); - } - } - - var keyStore = serviceProvider.GetService(); - if (keyStore != null) - { - dtlsServerBuilder.EnableExportOfSessionKeys(keyStore); - } - - foreach (var ca in listenEndpoint.EndpointConfig.ClientCAs) - { - dtlsServerBuilder.AddCA(ca); - } - - var channel = new UDPChannel(listenEndpoint.Endpoint); - var config = (CoapConfig)CoapConfig.Default; - // config.MaxMessageSize is used to check payload length - // we SHOULD store DTLSServer.MaxFragmentLength in block layer for each client - // to be able to determine how long the longest CoAP Message for a client is - // but this is too much of a refactoring at the moment - // thats why we use some buffer and only support FragmentLength defined by MaxMessageSize - // possible values are 128, 256 and 512 - // MaxMessageSize must be set to the least expected FragmentLength of the DTLS Clients - // Otherwise sending request wont work the FragmentLength is not in sync with the Blockwise layer - config.MaxMessageSize = options.MaxMessageSize - 64; - if(config.MaxMessageSize <= config.DefaultBlockSize) - { - config.DefaultBlockSize = config.MaxMessageSize / 2; - } - channel.ReceiveBufferSize = config.ChannelReceiveBufferSize; - channel.SendBufferSize = config.ChannelSendBufferSize; - channel.ReceivePacketSize = config.ChannelReceivePacketSize; - - var ep = new CoAPSEndpoint(serviceProvider.GetRequiredService(), dtlsServerBuilder.Config, channel, config); - //ep.Executor = new ThreadPoolExecutor(); - server.AddEndPoint(ep); - } - } - - - - return server; - } - } -} diff --git a/WorldDirect.CoAP/Tracing.cs b/WorldDirect.CoAP/Tracing.cs index b97ad15..16ba609 100644 --- a/WorldDirect.CoAP/Tracing.cs +++ b/WorldDirect.CoAP/Tracing.cs @@ -5,18 +5,17 @@ using System.Diagnostics; using System.Text; - internal static class Tracing + public static class Tracing { - public static readonly string ClientActivityName = "WorldDirect.CoAP.Client"; - public static readonly string ServerActivityName = "WorldDirect.CoAP.Server"; + public static readonly string ActivityName = "WorldDirect.CoAP"; /// /// The activity source for the tracing client events. /// - internal static readonly ActivitySource ClientSource = new ActivitySource(ClientActivityName, "1.0.0"); + 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(ServerActivityName, "1.0.0"); + 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 da80271..85a9c19 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,7 +2,7 @@ netstandard2.1 - 0.6.1-alpha33 + 0.6.1-alpha34 World-Direct eBusiness solutions GmbH 2021 LICENSE From ba2b92e991d59c86acb3deba4ceb4d4988bff228 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 22 May 2024 10:42:56 +0200 Subject: [PATCH 23/27] Update Bouncycastle because of Security Vulnability CVE-2024-30172 --- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 4 ++-- WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index f49c2c0..9dcf76f 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha34 + 0.6.3-alpha35 World-Direct eBusiness solutions GmbH 2023 LICENSE @@ -40,7 +40,7 @@ - + diff --git a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj index 5e81ad9..bfb8b2e 100644 --- a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj +++ b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha34 + 0.6.3-alpha35 World-Direct eBusiness solutions GmbH 2023 LICENSE From f087ad4ca005381c577a90bb874f712d65d9b0c5 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Wed, 10 Jul 2024 14:44:13 +0200 Subject: [PATCH 24/27] fix handshake timeout --- WorldDirect.CoAP.DTLS/DTLSSession.cs | 9 +++- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 51 ++++++++++++------- WorldDirect.CoAP.DTLS/UdpTransport.cs | 40 +++++++++++++-- .../WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Hosting.csproj | 2 +- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 6 files changed, 80 insertions(+), 26 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/DTLSSession.cs b/WorldDirect.CoAP.DTLS/DTLSSession.cs index 72273b1..cf20bfe 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSession.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSession.cs @@ -11,7 +11,7 @@ using WorldDirect.CoAP.Log; using WorldDirect.CoAP.Net; -internal class DTLSSession +internal class DTLSSession : IDisposable { private readonly DTLSSessionConfig config; private readonly UdpTransport transport; @@ -24,7 +24,7 @@ internal class DTLSSession public DTLSSession(IUDPSender sender, DTLSServer server, EndPoint remote, DTLSSessionConfig config) { this.config = config; - this.transport = new UdpTransport(sender, remote, config.MaxPacketLength); + this.transport = new UdpTransport(sender, remote, config.MaxPacketLength, config.HandshakeTimeout); this.protocol = new DtlsServerProtocol(); this.dtlsServer = server; this.logger = LogManager.GetLogger(); @@ -158,4 +158,9 @@ private void HandleSession() this.HandshakeFinished?.Invoke(this, new HandshakeFinishedEventArgs() { Successful = !this.HandshakeFailed }); } } + + public void Dispose() + { + this.transport.Dispose(); + } } diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 8f96aff..9e90b6d 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -7,8 +7,10 @@ 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; @@ -36,6 +38,7 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfi this.sender = sender; this.dtlsServerConfig = dtlsServerConfig; this.config = config; + this.log.LogDebug("Configured DTLS handshake timeout: {HandshakeTimeout}", dtlsServerConfig.HandshakeTimeout); } /// @@ -76,27 +79,38 @@ public void Stop() /// The endpoint who sent the packet. internal void ReceivedUdpPacket(ReadOnlySpan packet, EndPoint endPoint) { - var session = this.cache.GetOrCreate(GetKey(endPoint), entry => + 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)) { - entry.SlidingExpiration = config.SessionTimeout; - var callback = new PostEvictionCallbackRegistration() + session = this.cache.GetOrCreate(GetKey(endPoint), entry => { - EvictionCallback = OnEviction, - State = this - }; - entry.PostEvictionCallbacks.Add(callback); - entry.Priority = CacheItemPriority.NeverRemove; + 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; - }); - this.log.LogTrace("Received {Bytes} encrypted Bytes from {Remote}", packet.Length, endPoint); - session.Enqueue(packet); + 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) @@ -123,6 +137,7 @@ private static void OnEviction(object key, object value, EvictionReason reason, { 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/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs index 2d8e874..3e86aad 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -7,11 +7,14 @@ /// /// Represents the udp package buffer of a DTLS connection. /// -internal class UdpTransport : DatagramTransport +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. @@ -19,11 +22,13 @@ internal class UdpTransport : DatagramTransport /// The implementation of the udp /// The endpoint of the connection. /// The maximum length of a dtls package. - public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength) + /// 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; } /// @@ -61,6 +66,13 @@ public int Receive(byte[] buf, int off, int len, int waitMillis) /// 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); @@ -96,6 +108,11 @@ public void Send(byte[] buf, int off, int len) /// 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); } @@ -104,7 +121,6 @@ public void Send(ReadOnlySpan buffer) /// public void Close() { - } /// @@ -113,6 +129,24 @@ public void Close() /// 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 index 9dcf76f..af19dfb 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 0.6.3-alpha35 + 1.0.0-alpha.1 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj index bfb8b2e..7d56283 100644 --- a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj +++ b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.6.3-alpha35 + 1.0.0-alpha.1 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 85a9c19..70ade7c 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,7 +2,7 @@ netstandard2.1 - 0.6.1-alpha34 + 1.0.0-alpha.1 World-Direct eBusiness solutions GmbH 2021 LICENSE From d20beaa7249ac1e9476bbd808319c9ce09864966 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Fri, 9 Aug 2024 15:49:21 +0200 Subject: [PATCH 25/27] set initial timestamp --- WorldDirect.CoAP.DTLS/UdpTransport.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/WorldDirect.CoAP.DTLS/UdpTransport.cs b/WorldDirect.CoAP.DTLS/UdpTransport.cs index 3e86aad..a2aeb05 100644 --- a/WorldDirect.CoAP.DTLS/UdpTransport.cs +++ b/WorldDirect.CoAP.DTLS/UdpTransport.cs @@ -29,6 +29,7 @@ public UdpTransport(IUDPSender sender, EndPoint remote, int maxPacketLength, Tim this.sender = sender; this.maxPacketLength = maxPacketLength; this.timeout = timeout; + this.lastReceivedDatagram = DateTimeOffset.Now; } /// From a2e3cbb3dbed50d5851ce49732182f0b08962866 Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Fri, 9 Aug 2024 16:24:17 +0200 Subject: [PATCH 26/27] catch potential exception because of race condition --- WorldDirect.CoAP.DTLS/DTLSChannel.cs | 11 +++++++++-- WorldDirect.CoAP.DTLS/DTLSSessionManager.cs | 16 +++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/DTLSChannel.cs b/WorldDirect.CoAP.DTLS/DTLSChannel.cs index 6b27b71..a50a729 100644 --- a/WorldDirect.CoAP.DTLS/DTLSChannel.cs +++ b/WorldDirect.CoAP.DTLS/DTLSChannel.cs @@ -85,8 +85,15 @@ public void Stop() /// public void Send(byte[] data, EndPoint ep) { - this.logger.LogTrace("Sending {Bytes} decrypted bytes to {Remote}", data.Length, ep); - this.sessionManager.SendTo(data, 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) diff --git a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs index 9e90b6d..73f8bca 100644 --- a/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs +++ b/WorldDirect.CoAP.DTLS/DTLSSessionManager.cs @@ -54,13 +54,19 @@ public DTLSSessionManager(IMemoryCache cache, IUDPSender sender, DTLSServerConfi public void SendTo(ReadOnlySpan packet, EndPoint endPoint) { // cache.TryGetValue does not work (always returns false with null object...) - var session = this.cache.Get(GetKey(endPoint)); - if (session != null) + try { - session.Send(packet); - return; + 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); } From 14470ce22f925e8f7e17f3afa08d71f027a8e5fb Mon Sep 17 00:00:00 2001 From: Lukas Karel Date: Thu, 9 Jan 2025 10:59:30 +0100 Subject: [PATCH 27/27] fix crash when receiving large udp messages --- WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs | 6 +----- WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj | 2 +- .../WorldDirect.CoAP.Hosting.csproj | 2 +- WorldDirect.CoAP/Channel/UDPChannel.NET40.cs | 12 ++++++++++-- WorldDirect.CoAP/Channel/UDPChannel.cs | 6 ++++++ WorldDirect.CoAP/CoapConfig.cs | 2 +- WorldDirect.CoAP/WorldDirect.CoAP.csproj | 2 +- 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs index 569d41d..ccbdcdd 100644 --- a/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs +++ b/WorldDirect.CoAP.DTLS/CoAPSEndPoint.cs @@ -66,8 +66,6 @@ public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, ICoapConfi _matcher = new Matcher(this._config); _coapStack = new CoapStack(this._config); UDPChannel channel = new UDPChannel(new IPEndPoint(IPAddress.Any, 5684)); - channel.ReceiveBufferSize = this._config.ChannelReceiveBufferSize; - channel.SendBufferSize = this._config.ChannelSendBufferSize; channel.ReceivePacketSize = this._config.ChannelReceivePacketSize; this.channel = new DTLSChannel(channel, cache, dtlsConfig); this.channel.DtlsDataReceived += Channel_DataReceived; @@ -90,9 +88,7 @@ public CoAPSEndpoint(IMemoryCache cache, DTLSServerConfig dtlsConfig, IPEndPoint _matcher = new Matcher(this._config); _coapStack = new CoapStack(this._config); var udpChannel = new UDPChannel(endpoint); - - // DTLS Header has 9 bytes - udpChannel.ReceivePacketSize = config.MaxMessageSize + 9; + udpChannel.ReceivePacketSize = this._config.ChannelReceivePacketSize; this.channel = new DTLSChannel(udpChannel, cache, dtlsConfig); this.channel.DtlsDataReceived += Channel_DataReceived; this.DTLSConfig = dtlsConfig; diff --git a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj index af19dfb..8ebf28e 100644 --- a/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj +++ b/WorldDirect.CoAP.DTLS/WorldDirect.CoAP.DTLS.csproj @@ -3,7 +3,7 @@ net6.0 enable enable - 1.0.0-alpha.1 + 1.0.0-alpha.3 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj index 7d56283..0db6406 100644 --- a/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj +++ b/WorldDirect.CoAP.Hosting/WorldDirect.CoAP.Hosting.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 1.0.0-alpha.1 + 1.0.0-alpha.3 World-Direct eBusiness solutions GmbH 2023 LICENSE diff --git a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs index ab90533..5a0592d 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.NET40.cs @@ -78,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 0c7aadf..bb98db4 100644 --- a/WorldDirect.CoAP/Channel/UDPChannel.cs +++ b/WorldDirect.CoAP/Channel/UDPChannel.cs @@ -111,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() { 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/WorldDirect.CoAP.csproj b/WorldDirect.CoAP/WorldDirect.CoAP.csproj index 70ade7c..e94a0fd 100644 --- a/WorldDirect.CoAP/WorldDirect.CoAP.csproj +++ b/WorldDirect.CoAP/WorldDirect.CoAP.csproj @@ -2,7 +2,7 @@ netstandard2.1 - 1.0.0-alpha.1 + 1.0.0-alpha.3 World-Direct eBusiness solutions GmbH 2021 LICENSE