From 88aa58305eccb5732b072da31e4116b6bf1666ae Mon Sep 17 00:00:00 2001
From: Haves Irfan <20160532+ha-ves@users.noreply.github.com>
Date: Sat, 15 Nov 2025 01:28:22 +0900
Subject: [PATCH] * Initial .NET 10 support * Use 'latest' C# language version
* TFM conditional compilation * update packages * additional extensions
---
src/SIPSorcery.csproj | 8 ++++----
.../Media/Sources/VideoTestPatternSource.cs | 4 ++++
src/core/SIP/Channels/SIPTLSChannel.cs | 12 ++++++++++--
src/core/SIPTransportConfig.cs | 4 ++++
src/net/DtlsSrtp/DtlsUtils.cs | 4 ++++
src/net/ICE/IceChecklistEntry.cs | 2 +-
src/net/ICE/IceServer.cs | 4 ++--
.../STUNAttributes/STUNXORAddressAttribute.cs | 2 +-
src/net/TURN/TurnClient.cs | 4 ++--
src/sys/Crypto/Crypto.cs | 4 ++++
src/sys/Crypto/PasswordHash.cs | 19 +++++++------------
src/sys/Net/NetConvert.cs | 10 +++++-----
src/sys/TypeExtensions.cs | 6 ++++++
.../SIPSorcery.IntegrationTests.csproj | 2 +-
.../core/SIPTransportIntegrationTest.cs | 18 ++++++++++++++++++
.../net/DtlsSrtp/DtlsUtilsUnitTest.cs | 12 ++++++++++++
test/unit/SIPSorcery.UnitTests.csproj | 2 +-
17 files changed, 86 insertions(+), 31 deletions(-)
diff --git a/src/SIPSorcery.csproj b/src/SIPSorcery.csproj
index af4e9984bc..27c3035546 100755
--- a/src/SIPSorcery.csproj
+++ b/src/SIPSorcery.csproj
@@ -19,7 +19,7 @@
-
+
@@ -29,15 +29,15 @@
-
+
- netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0
- 12.0
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0
+ latest
true
$(NoWarn);SYSLIB0050
True
diff --git a/src/app/Media/Sources/VideoTestPatternSource.cs b/src/app/Media/Sources/VideoTestPatternSource.cs
index f7f2d88570..771f73df89 100755
--- a/src/app/Media/Sources/VideoTestPatternSource.cs
+++ b/src/app/Media/Sources/VideoTestPatternSource.cs
@@ -95,7 +95,11 @@ public VideoTestPatternSource(IVideoEncoder encoder = null)
else
{
_testI420Buffer = new byte[TEST_PATTERN_WIDTH * TEST_PATTERN_HEIGHT * 3 / 2];
+#if NET9_0_OR_GREATER
+ testPatternStm.ReadExactly(_testI420Buffer, 0, _testI420Buffer.Length);
+#else
testPatternStm.Read(_testI420Buffer, 0, _testI420Buffer.Length);
+#endif
testPatternStm.Close();
_sendTestPatternTimer = new Timer(GenerateTestPattern, null, Timeout.Infinite, Timeout.Infinite);
_frameSpacing = 1000 / DEFAULT_FRAMES_PER_SECOND;
diff --git a/src/core/SIP/Channels/SIPTLSChannel.cs b/src/core/SIP/Channels/SIPTLSChannel.cs
index ad149c46ea..42973bcb0c 100644
--- a/src/core/SIP/Channels/SIPTLSChannel.cs
+++ b/src/core/SIP/Channels/SIPTLSChannel.cs
@@ -355,7 +355,15 @@ private void DisplayCertificateChain(X509Certificate2 certificate)
private void DisplaySecurityLevel(SslStream stream)
{
- logger.LogDebug("Cipher: {CipherAlgorithm} strength {CipherStrength}, Hash: {HashAlgorithm} strength {HashStrength}, Key exchange: {KeyExchangeAlgorithm} strength {KeyExchangeStrength}, Protocol: {SslProtocol}", stream.CipherAlgorithm, stream.CipherStrength, stream.HashAlgorithm, stream.HashStrength, stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength, stream.SslProtocol);
+#if NET5_0_OR_GREATER
+ // Use the negotiated cipher suite property available in .NET 5+.
+ var cipherSuite = stream.NegotiatedCipherSuite;
+ logger.LogDebug("Negotiated cipher suite: {CipherSuite}, Protocol: {SslProtocol}", cipherSuite, stream.SslProtocol);
+#else
+ logger.LogDebug("Cipher: {CipherAlgorithm} strength {CipherStrength}, Hash: {HashAlgorithm} strength {HashStrength}, Key exchange: {KeyExchangeAlgorithm} strength {KeyExchangeStrength}, Protocol: {SslProtocol}",
+ stream.CipherAlgorithm, stream.CipherStrength, stream.HashAlgorithm, stream.HashStrength, stream.KeyExchangeAlgorithm, stream.KeyExchangeStrength, stream.SslProtocol
+ );
+#endif
}
private void DisplaySecurityServices(SslStream stream)
@@ -399,6 +407,6 @@ private void DisplayCertificateInformation(SslStream stream)
}
}
- #endregion
+#endregion
}
}
diff --git a/src/core/SIPTransportConfig.cs b/src/core/SIPTransportConfig.cs
index 1ea5e70cca..be494f8f3c 100644
--- a/src/core/SIPTransportConfig.cs
+++ b/src/core/SIPTransportConfig.cs
@@ -126,7 +126,11 @@ private static X509Certificate2 LoadCertificate(string certificateType, string c
if (certificateType == "file")
{
+#if NET9_0_OR_GREATER
+ var serverCertificate = X509CertificateLoader.LoadPkcs12FromFile(certifcateLocation, certKeyPassword);
+#else
var serverCertificate = new X509Certificate2(certifcateLocation, certKeyPassword);
+#endif
//DisplayCertificateChain(m_serverCertificate);
var verifyCert = serverCertificate.Verify();
logger.LogDebug("Server Certificate loaded from file, Subject={Subject}, valid={Valid}.", serverCertificate.Subject, verifyCert);
diff --git a/src/net/DtlsSrtp/DtlsUtils.cs b/src/net/DtlsSrtp/DtlsUtils.cs
index 09f5d37ac7..95a2224464 100755
--- a/src/net/DtlsSrtp/DtlsUtils.cs
+++ b/src/net/DtlsSrtp/DtlsUtils.cs
@@ -435,7 +435,11 @@ public static X509Certificate2 ConvertBouncyCert(Org.BouncyCastle.X509.X509Certi
{
pkcs12Store.Save(pfxStream, new char[] { }, new SecureRandom());
pfxStream.Seek(0, SeekOrigin.Begin);
+#if NET9_0_OR_GREATER
+ keyedCert = X509CertificateLoader.LoadPkcs12(pfxStream.ToArray(), string.Empty, X509KeyStorageFlags.Exportable);
+#else
keyedCert = new X509Certificate2(pfxStream.ToArray(), string.Empty, X509KeyStorageFlags.Exportable);
+#endif
}
return keyedCert;
diff --git a/src/net/ICE/IceChecklistEntry.cs b/src/net/ICE/IceChecklistEntry.cs
index 40cdff5453..79765619c4 100644
--- a/src/net/ICE/IceChecklistEntry.cs
+++ b/src/net/ICE/IceChecklistEntry.cs
@@ -310,7 +310,7 @@ internal void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
if (lifetime != null)
{
LocalCandidate.IceServer.TurnTimeToExpiry = DateTime.Now +
- TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.Reverse().ToArray(), 0));
+ TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.FluentReverse().ToArray(), 0));
}
}
else if (stunResponse.Header.MessageType == STUNMessageTypesEnum.RefreshErrorResponse)
diff --git a/src/net/ICE/IceServer.cs b/src/net/ICE/IceServer.cs
index ea35ec52a9..71710c6874 100755
--- a/src/net/ICE/IceServer.cs
+++ b/src/net/ICE/IceServer.cs
@@ -428,7 +428,7 @@ internal bool GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
if (lifetime != null)
{
TurnTimeToExpiry = DateTime.Now +
- TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.Reverse().ToArray(), 0));
+ TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.FluentReverse().ToArray(), 0));
}
else
{
@@ -536,7 +536,7 @@ internal bool GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoin
if (lifetime != null)
{
TurnTimeToExpiry = DateTime.Now +
- TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.Reverse().ToArray(), 0));
+ TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetime.Value.FluentReverse().ToArray(), 0));
}
}
diff --git a/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs b/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs
index c6cc025c0e..5ff64d7d1c 100644
--- a/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs
+++ b/src/net/STUN/STUNAttributes/STUNXORAddressAttribute.cs
@@ -59,7 +59,7 @@ public STUNXORAddressAttribute(STUNAttributeTypesEnum attributeType, byte[] attr
if (BitConverter.IsLittleEndian)
{
Port = NetConvert.DoReverseEndian(BitConverter.ToUInt16(attributeValue, 2)) ^ (UInt16)(STUNHeader.MAGIC_COOKIE >> 16);
- address = BitConverter.GetBytes(NetConvert.DoReverseEndian(BitConverter.ToUInt32(attributeValue, 4)) ^ STUNHeader.MAGIC_COOKIE).Reverse().ToArray();
+ address = BitConverter.GetBytes(NetConvert.DoReverseEndian(BitConverter.ToUInt32(attributeValue, 4)) ^ STUNHeader.MAGIC_COOKIE).FluentReverse().ToArray();
}
else
{
diff --git a/src/net/TURN/TurnClient.cs b/src/net/TURN/TurnClient.cs
index f4be0ff4c3..eb17956696 100644
--- a/src/net/TURN/TurnClient.cs
+++ b/src/net/TURN/TurnClient.cs
@@ -271,7 +271,7 @@ private void GotStunResponse(STUNMessage stunResponse, IPEndPoint remoteEndPoint
if (permissionLifetime != null)
{
- permissionDuration = TimeSpan.FromSeconds(BitConverter.ToUInt32(permissionLifetime.Value.Reverse().ToArray(), 0));
+ permissionDuration = TimeSpan.FromSeconds(BitConverter.ToUInt32(permissionLifetime.Value.FluentReverse().ToArray(), 0));
logger.LogDebug("TURN permission lifetime attribute value {lifetimeSeconds}s.", permissionDuration.TotalSeconds);
}
@@ -349,7 +349,7 @@ private void ScheduleAllocateRefresh(STUNAttribute lifetimeAttribute)
if (lifetimeAttribute != null)
{
- var lifetimeSpan = TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetimeAttribute.Value.Reverse().ToArray(), 0));
+ var lifetimeSpan = TimeSpan.FromSeconds(BitConverter.ToUInt32(lifetimeAttribute.Value.FluentReverse().ToArray(), 0));
logger.LogDebug("TURN allocate lifetime attribute value {lifetimeSeconds}s.", lifetimeSpan.TotalSeconds);
diff --git a/src/sys/Crypto/Crypto.cs b/src/sys/Crypto/Crypto.cs
index 99e7dbb4fd..f8b6ba10ba 100644
--- a/src/sys/Crypto/Crypto.cs
+++ b/src/sys/Crypto/Crypto.cs
@@ -269,7 +269,11 @@ public static string GetHash(string filepath)
// Buffer to read in plain text blocks.
byte[] fileBuffer = new byte[fileStream.Length];
+#if NET9_0_OR_GREATER
+ fileStream.ReadExactly(fileBuffer, 0, (int)fileStream.Length);
+#else
fileStream.Read(fileBuffer, 0, (int)fileStream.Length);
+#endif
fileStream.Close();
byte[] overallHash = shaM.ComputeHash(fileBuffer);
diff --git a/src/sys/Crypto/PasswordHash.cs b/src/sys/Crypto/PasswordHash.cs
index 6dacb3b4c5..10c4fe27f6 100755
--- a/src/sys/Crypto/PasswordHash.cs
+++ b/src/sys/Crypto/PasswordHash.cs
@@ -66,22 +66,17 @@ public static string Hash(string value, string salt)
salt = salt.Substring(i + 1);
byte[] key = null;
+ byte[] saltBytes = Convert.FromBase64String(salt);
+
#if NET8_0_OR_GREATER
- // Use the updated constructor for .NET 8.0 or later
- using (var pbkdf2 = new Rfc2898DeriveBytes(
- Encoding.UTF8.GetBytes(value),
- Convert.FromBase64String(salt),
- iters,
- HashAlgorithmName.SHA256))
+ // Use PBKDF2 with SHA256 on .NET 8+.
+ key = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(value), saltBytes, iters, System.Security.Cryptography.HashAlgorithmName.SHA256, 24);
+#else
+ // Fallback to the existing approach for .NET Framework and .NET Standard
+ using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(value), saltBytes, iters))
{
key = pbkdf2.GetBytes(24);
}
-#else
- // Fallback to the existing approach for .NET Framework and .NET Standard
- using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(value), Convert.FromBase64String(salt), iters))
- {
- key = pbkdf2.GetBytes(24);
- }
#endif
return Convert.ToBase64String(key);
diff --git a/src/sys/Net/NetConvert.cs b/src/sys/Net/NetConvert.cs
index 8c223f9d4c..31a27de100 100644
--- a/src/sys/Net/NetConvert.cs
+++ b/src/sys/Net/NetConvert.cs
@@ -1,4 +1,4 @@
-//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
// Filename: Utilities.cs
//
// Description: Useful functions for VoIP protocol implementation.
@@ -23,24 +23,24 @@ public class NetConvert
public static UInt16 DoReverseEndian(UInt16 x)
{
//return Convert.ToUInt16((x << 8 & 0xff00) | (x >> 8));
- return BitConverter.ToUInt16(BitConverter.GetBytes(x).Reverse().ToArray(), 0);
+ return BitConverter.ToUInt16(BitConverter.GetBytes(x).FluentReverse().ToArray(), 0);
}
public static uint DoReverseEndian(uint x)
{
//return (x << 24 | (x & 0xff00) << 8 | (x & 0xff0000) >> 8 | x >> 24);
- return BitConverter.ToUInt32(BitConverter.GetBytes(x).Reverse().ToArray(), 0);
+ return BitConverter.ToUInt32(BitConverter.GetBytes(x).FluentReverse().ToArray(), 0);
}
public static ulong DoReverseEndian(ulong x)
{
//return (x << 56 | (x & 0xff00) << 40 | (x & 0xff0000) << 24 | (x & 0xff000000) << 8 | (x & 0xff00000000) >> 8 | (x & 0xff0000000000) >> 24 | (x & 0xff000000000000) >> 40 | x >> 56);
- return BitConverter.ToUInt64(BitConverter.GetBytes(x).Reverse().ToArray(), 0);
+ return BitConverter.ToUInt64(BitConverter.GetBytes(x).FluentReverse().ToArray(), 0);
}
public static int DoReverseEndian(int x)
{
- return BitConverter.ToInt32(BitConverter.GetBytes(x).Reverse().ToArray(), 0);
+ return BitConverter.ToInt32(BitConverter.GetBytes(x).FluentReverse().ToArray(), 0);
}
///
diff --git a/src/sys/TypeExtensions.cs b/src/sys/TypeExtensions.cs
index 47d7e29998..0fdc5f3cc9 100755
--- a/src/sys/TypeExtensions.cs
+++ b/src/sys/TypeExtensions.cs
@@ -50,6 +50,12 @@ public static class TypeExtensions
private static readonly char[] hexmap = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ public static Span FluentReverse(this Span span)
+ {
+ span.Reverse();
+ return span;
+ }
+
///
/// Gets a value that indicates whether or not the string is empty.
///
diff --git a/test/integration/SIPSorcery.IntegrationTests.csproj b/test/integration/SIPSorcery.IntegrationTests.csproj
index 79a95c5dae..a71896afb7 100755
--- a/test/integration/SIPSorcery.IntegrationTests.csproj
+++ b/test/integration/SIPSorcery.IntegrationTests.csproj
@@ -1,7 +1,7 @@
- net462;net8.0
+ net462;net8.0;net9;net10.0
false
true
True
diff --git a/test/integration/core/SIPTransportIntegrationTest.cs b/test/integration/core/SIPTransportIntegrationTest.cs
index 14fcc89eef..171e620321 100644
--- a/test/integration/core/SIPTransportIntegrationTest.cs
+++ b/test/integration/core/SIPTransportIntegrationTest.cs
@@ -351,7 +351,13 @@ public async Task IPv6TlsLoopbackSendReceiveTest()
Assert.True(File.Exists(@"certs/localhost.pfx"), "The TLS transport channel test was missing the localhost.pfx certificate file.");
+#if NET9_0_OR_GREATER
+ var serverCertificate = X509CertificateLoader.LoadPkcs12FromFile(@"certs/localhost.pfx", "");
+#else
+#pragma warning disable SYSLIB0057 // X509Certificate2 constructor is obsolete in NET9+
var serverCertificate = new X509Certificate2(@"certs/localhost.pfx", "");
+#pragma warning restore SYSLIB0057
+#endif
var verifyCert = serverCertificate.Verify();
logger.LogDebug("Server Certificate loaded from file, Subject={Subject}, valid={Valid}.", serverCertificate.Subject, verifyCert);
@@ -416,7 +422,13 @@ public async Task IPv4TlsLoopbackSendReceiveTest()
Assert.True(File.Exists(@"certs/localhost.pfx"), "The TLS transport channel test was missing the localhost.pfx certificate file.");
+#if NET9_0_OR_GREATER
+ var serverCertificate = X509CertificateLoader.LoadPkcs12FromFile(@"certs/localhost.pfx", "");
+#else
+#pragma warning disable SYSLIB0057 // X509Certificate2 constructor is obsolete in NET9+
var serverCertificate = new X509Certificate2(@"certs/localhost.pfx", "");
+#pragma warning restore SYSLIB0057
+#endif
var verifyCert = serverCertificate.Verify();
logger.LogDebug("Server Certificate loaded from file, Subject={Subject}, valid={Valid}.", serverCertificate.Subject, verifyCert);
@@ -686,7 +698,13 @@ public async Task TlsDoesNotGetStuckOnIncompleteTcpConnection()
TaskCompletionSource testComplete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Assert.True(File.Exists(@"certs/localhost.pfx"), "The TLS transport channel test was missing the localhost.pfx certificate file.");
+#if NET9_0_OR_GREATER
+ var serverCertificate = X509CertificateLoader.LoadPkcs12FromFile(@"certs/localhost.pfx", "");
+#else
+#pragma warning disable SYSLIB0057 // X509Certificate2 constructor is obsolete in NET9+
var serverCertificate = new X509Certificate2(@"certs/localhost.pfx", "");
+#pragma warning restore SYSLIB0057
+#endif
serverCertificate.Verify();
var serverChannel = new SIPTLSChannel(serverCertificate, IPAddress.Loopback, 0);
diff --git a/test/integration/net/DtlsSrtp/DtlsUtilsUnitTest.cs b/test/integration/net/DtlsSrtp/DtlsUtilsUnitTest.cs
index 34b701f8dc..df557b5bbf 100644
--- a/test/integration/net/DtlsSrtp/DtlsUtilsUnitTest.cs
+++ b/test/integration/net/DtlsSrtp/DtlsUtilsUnitTest.cs
@@ -90,7 +90,13 @@ public void LoadSecretFromArchiveUnitTest()
return;
}
#endif
+#if NET9_0_OR_GREATER
+ var cert = X509CertificateLoader.LoadPkcs12FromFile("certs/localhost.pfx", string.Empty, X509KeyStorageFlags.Exportable);
+#else
+#pragma warning disable SYSLIB0057 // X509Certificate2 constructor is obsolete in NET9+
var cert = new X509Certificate2("certs/localhost.pfx", string.Empty, X509KeyStorageFlags.Exportable);
+#pragma warning restore SYSLIB0057
+#endif
Assert.NotNull(cert);
var key = DtlsUtils.LoadPrivateKeyResource(cert);
Assert.NotNull(key);
@@ -114,7 +120,13 @@ public void BouncyCertFromCoreFxCert()
}
#endif
+#if NET9_0_OR_GREATER
+ var coreFxCert = X509CertificateLoader.LoadPkcs12FromFile("certs/localhost.pfx", string.Empty, X509KeyStorageFlags.Exportable);
+#else
+#pragma warning disable SYSLIB0057 // X509Certificate2 constructor is obsolete in NET9+
var coreFxCert = new X509Certificate2("certs/localhost.pfx", string.Empty, X509KeyStorageFlags.Exportable);
+#pragma warning restore SYSLIB0057
+#endif
Assert.NotNull(coreFxCert);
Assert.NotNull(coreFxCert.PrivateKey);
diff --git a/test/unit/SIPSorcery.UnitTests.csproj b/test/unit/SIPSorcery.UnitTests.csproj
index 256f16cadb..e37e155486 100755
--- a/test/unit/SIPSorcery.UnitTests.csproj
+++ b/test/unit/SIPSorcery.UnitTests.csproj
@@ -1,7 +1,7 @@
- net462;net8.0
+ net462;net8.0;net9;net10.0
false
true
True