Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/SIPSorcery.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Concentus" Version="2.2.2" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.16" />
<PackageReference Include="SIPSorcery.WebSocketSharp" Version="0.0.1" />
<PackageReference Include="SIPSorceryMedia.Abstractions" Version="8.0.12" />
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
Expand All @@ -35,15 +36,22 @@
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.net462" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<NoWarn>$(NoWarn);SYSLIB0050</NoWarn>
<NoWarn>$(NoWarn);SYSLIB0050;CS1591;CS1573;CS1587</NoWarn>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1510;CS0809;CS0618;CS8632</WarningsNotAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Disable warning for missing XML doc comments. -->
<NoWarn>$(NoWarn);CS1591;CS1573;CS1587</NoWarn>
<Authors>Aaron Clauson, Christophe Irles, Rafael Soares &amp; Contributors</Authors>
<Copyright>Copyright © 2010-2025 Aaron Clauson</Copyright>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
Expand Down Expand Up @@ -94,6 +102,7 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<AllowUnsafeBlocks Condition="'$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'netstandard2.0'">true</AllowUnsafeBlocks>
</PropertyGroup>

</Project>
130 changes: 111 additions & 19 deletions src/core/SIP/SIPAuthorisationDigest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,24 +256,116 @@ public string GetDigest()

public override string ToString()
{
string authHeader = AuthHeaders.AUTH_DIGEST_KEY + " ";

authHeader += (Username != null && Username.Trim().Length != 0) ? AuthHeaders.AUTH_USERNAME_KEY + "=\"" + Username + "\"" : null;
authHeader += (authHeader.IndexOf('=') != -1) ? "," + AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\"" : AuthHeaders.AUTH_REALM_KEY + "=\"" + Realm + "\"";
authHeader += (Nonce != null) ? "," + AuthHeaders.AUTH_NONCE_KEY + "=\"" + Nonce + "\"" : null;
authHeader += (URI != null && URI.Trim().Length != 0) ? "," + AuthHeaders.AUTH_URI_KEY + "=\"" + URI + "\"" : null;
authHeader += (Response != null && Response.Length != 0) ? "," + AuthHeaders.AUTH_RESPONSE_KEY + "=\"" + Response + "\"" : null;
authHeader += (Cnonce != null) ? "," + AuthHeaders.AUTH_CNONCE_KEY + "=\"" + Cnonce + "\"" : null;
authHeader += (NonceCount != 0) ? "," + AuthHeaders.AUTH_NONCECOUNT_KEY + "=" + GetPaddedNonceCount(NonceCount) : null;
authHeader += (Qop != null) ? "," + AuthHeaders.AUTH_QOP_KEY + "=" + Qop : null;
authHeader += (Opaque != null) ? "," + AuthHeaders.AUTH_OPAQUE_KEY + "=\"" + Opaque + "\"" : null;

string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256) ? SHA256_ALGORITHM_ID : DigestAlgorithm.ToString();
authHeader += (Response != null) ? "," + AuthHeaders.AUTH_ALGORITHM_KEY + "=" + algorithmID : null;

return authHeader;
var builder = new ValueStringBuilder();

try
{
ToString(ref builder);

return builder.ToString();
}
finally
{
builder.Dispose();
}
}

internal void ToString(ref ValueStringBuilder builder)
{
builder.Append(AuthHeaders.AUTH_DIGEST_KEY);
builder.Append(' ');

bool hasUsername = !string.IsNullOrWhiteSpace(Username);
if (hasUsername)
{
builder.Append(AuthHeaders.AUTH_USERNAME_KEY);
builder.Append("=\"");
builder.Append(Username);
builder.Append('"');
}

builder.Append(hasUsername ? ',' : '\0');
builder.Append(AuthHeaders.AUTH_REALM_KEY);
builder.Append("=\"");
builder.Append(Realm);
builder.Append('"');

if (Nonce != null)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_NONCE_KEY);
builder.Append("=\"");
builder.Append(Nonce);
builder.Append('"');
}

if (!string.IsNullOrWhiteSpace(URI))
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_URI_KEY);
builder.Append("=\"");
builder.Append(URI);
builder.Append('"');
}

if (!string.IsNullOrEmpty(Response))
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_RESPONSE_KEY);
builder.Append("=\"");
builder.Append(Response);
builder.Append('"');
}

if (Cnonce != null)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_CNONCE_KEY);
builder.Append("=\"");
builder.Append(Cnonce);
builder.Append('"');
}

if (NonceCount != 0)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_NONCECOUNT_KEY);
builder.Append('=');
builder.Append(GetPaddedNonceCount(NonceCount));
}

if (Qop != null)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_QOP_KEY);
builder.Append('=');
builder.Append(Qop);
}

if (Opaque != null)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_OPAQUE_KEY);
builder.Append("=\"");
builder.Append(Opaque);
builder.Append('"');
}

if (Response != null)
{
builder.Append(',');
builder.Append(AuthHeaders.AUTH_ALGORITHM_KEY);
builder.Append('=');

string algorithmID = (DigestAlgorithm == DigestAlgorithmsEnum.SHA256)
? SHA256_ALGORITHM_ID
: DigestAlgorithm.ToString();

builder.Append(algorithmID);
}
}


public SIPAuthorisationDigest CopyOf()
{
var copy = new SIPAuthorisationDigest(AuthorisationType, Realm, Username, Password, URI, Nonce, RequestType, DigestAlgorithm);
Expand Down Expand Up @@ -385,21 +477,21 @@ public static string GetHashHex(DigestAlgorithmsEnum hashAlg, string val)
case DigestAlgorithmsEnum.SHA256:
using (var hash = new SHA256CryptoServiceProvider())
{
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true);
}
// This is commented because RFC8760 does not have an SHA-512 option. Instead it's HSA-512-sess which
// means the SIP request body needs to be included in the digest as well. Including the body will require
// some additional changes that can be done at a later date.
//case DigestAlgorithmsEnum.SHA512:
// using (var hash = new SHA512CryptoServiceProvider())
// {
// return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
// return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr(lowercase: false);
// }
case DigestAlgorithmsEnum.MD5:
default:
using (var hash = new MD5CryptoServiceProvider())
{
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).HexStr().ToLower();
return hash.ComputeHash(Encoding.UTF8.GetBytes(val)).AsSpan().HexStr(lowercase: true);
}
}
#pragma warning restore SYSLIB0021
Expand Down
69 changes: 60 additions & 9 deletions src/core/SIP/SIPConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -541,17 +541,68 @@ public static string SIPURIUserUnescape(string escapedString)

public static string SIPURIParameterEscape(string unescapedString)
{
string result = unescapedString;
if (!result.IsNullOrBlank())
// Characters that need escaping
ReadOnlySpan<char> specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' };

// Early exit if no special characters are found
var unescapedSpan = unescapedString.AsSpan();
int nextIndex = unescapedSpan.IndexOfAny(specialChars);
if (nextIndex == -1)
{
result = result.Replace(";", "%3B");
result = result.Replace("?", "%3F");
result = result.Replace("@", "%40");
result = result.Replace("=", "%3D");
result = result.Replace(",", "%2C");
result = result.Replace(" ", "%20");
// No escaping needed
return unescapedString;
}

var builder = new ValueStringBuilder();

try
{
SIPURIParameterEscape(ref builder, unescapedSpan);

return builder.ToString();
}
finally
{
builder.Dispose();
}
}

internal static void SIPURIParameterEscape(ref ValueStringBuilder builder, ReadOnlySpan<char> unescapedSpan)
{
// Characters that need escaping
ReadOnlySpan<char> specialChars = stackalloc char[] { ';', '?', '@', '=', ',', ' ' };

var currentIndex = 0;
var nextIndex = unescapedSpan.IndexOfAny(specialChars);
while (nextIndex != -1)
{
// Append everything before the special character
builder.Append(unescapedSpan.Slice(currentIndex, nextIndex - currentIndex));

// Escape the special character
switch (unescapedSpan[nextIndex])
{
case ';': builder.Append("%3B"); break;
case '?': builder.Append("%3F"); break;
case '@': builder.Append("%40"); break;
case '=': builder.Append("%3D"); break;
case ',': builder.Append("%2C"); break;
case ' ': builder.Append("%20"); break;
}

currentIndex = nextIndex + 1;
nextIndex = unescapedSpan.Slice(currentIndex).IndexOfAny(specialChars);
if (nextIndex != -1)
{
nextIndex += currentIndex; // Adjust relative index to absolute
}
}

// Append the remaining part
if (currentIndex < unescapedSpan.Length)
{
builder.Append(unescapedSpan.Slice(currentIndex));
}
return result;
}

public static string SIPURIParameterUnescape(string escapedString)
Expand Down
Loading