Skip to content

Commit aada351

Browse files
committed
Refactor SIP authorization digest parsing in SIPAuthorisationDigest.cs to use ReadOnlySpan<char> for improved performance and memory efficiency. Simplified header field handling by replacing regular expressions and string arrays with spans and slices. Expanded unit tests in SIPAuthorisationDigestUnitTest.cs to cover various scenarios, including basic, full, malformed, and case-insensitive headers, as well as nonce counts and QOP values.
1 parent b32de41 commit aada351

File tree

2 files changed

+191
-53
lines changed

2 files changed

+191
-53
lines changed

src/core/SIP/SIPAuthorisationDigest.cs

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -85,69 +85,103 @@ public static SIPAuthorisationDigest ParseAuthorisationDigest(SIPAuthorisationHe
8585
{
8686
SIPAuthorisationDigest authRequest = new SIPAuthorisationDigest(authorisationType);
8787

88-
string noDigestHeader = Regex.Replace(authorisationRequest, $@"^\s*{METHOD}\s*", "", RegexOptions.IgnoreCase);
89-
string[] headerFields = noDigestHeader.Split(',');
88+
//string noDigestHeader = Regex.Replace(authorisationRequest, $@"^\s*{METHOD}\s*", "", RegexOptions.IgnoreCase);
89+
ReadOnlySpan<char> noDigestHeader = authorisationRequest.AsSpan().Trim();
90+
if (noDigestHeader.StartsWith(METHOD.AsSpan(), StringComparison.OrdinalIgnoreCase))
91+
{
92+
noDigestHeader = noDigestHeader.Slice(METHOD.Length + 1);
93+
}
9094

91-
if (headerFields != null && headerFields.Length > 0)
95+
while (noDigestHeader.IsEmpty == false)
9296
{
93-
foreach (string headerField in headerFields)
97+
ReadOnlySpan<char> headerField;
98+
int commaIndex = noDigestHeader.IndexOf(',');
99+
if (commaIndex == -1)
100+
{
101+
headerField = noDigestHeader.Trim();
102+
noDigestHeader = ReadOnlySpan<char>.Empty;
103+
}
104+
else
105+
{
106+
headerField = noDigestHeader.Slice(0, commaIndex).Trim();
107+
noDigestHeader = noDigestHeader.Slice(commaIndex + 1);
108+
}
109+
110+
int equalsIndex = headerField.IndexOf('=');
111+
112+
if (equalsIndex != -1 && equalsIndex < headerField.Length)
94113
{
95-
int equalsIndex = headerField.IndexOf('=');
114+
ReadOnlySpan<char> headerName = headerField.Slice(0, equalsIndex).Trim();
115+
ReadOnlySpan<char> headerValue = headerField.Slice(equalsIndex + 1).Trim(m_headerFieldRemoveChars);
96116

97-
if (equalsIndex != -1 && equalsIndex < headerField.Length)
117+
if (headerName.Equals(AuthHeaders.AUTH_REALM_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
118+
{
119+
authRequest.Realm = headerValue.ToString();
120+
}
121+
else if (headerName.Equals(AuthHeaders.AUTH_NONCE_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
122+
{
123+
authRequest.Nonce = headerValue.ToString();
124+
}
125+
else if (headerName.Equals(AuthHeaders.AUTH_USERNAME_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
126+
{
127+
authRequest.Username = headerValue.ToString();
128+
}
129+
else if (headerName.Equals(AuthHeaders.AUTH_RESPONSE_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
130+
{
131+
authRequest.Response = headerValue.ToString();
132+
}
133+
else if (headerName.Equals(AuthHeaders.AUTH_URI_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
134+
{
135+
authRequest.URI = headerValue.ToString();
136+
}
137+
else if (headerName.Equals(AuthHeaders.AUTH_CNONCE_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
138+
{
139+
authRequest.Cnonce = headerValue.ToString();
140+
}
141+
else if (headerName.Equals(AuthHeaders.AUTH_NONCECOUNT_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
98142
{
99-
string headerName = headerField.Substring(0, equalsIndex).Trim();
100-
string headerValue = headerField.Substring(equalsIndex + 1).Trim(m_headerFieldRemoveChars);
143+
#if NETCOREAPP2_1_OR_GREATER
144+
Int32.TryParse(headerValue, out authRequest.NonceCount);
145+
#else
146+
Int32.TryParse(headerValue.ToString(), out authRequest.NonceCount);
147+
#endif
148+
}
149+
else if (headerName.Equals(AuthHeaders.AUTH_QOP_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
150+
{
151+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
152+
authRequest.Qop = BuildQop(headerValue);
101153

102-
if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_REALM_KEY + "$", RegexOptions.IgnoreCase).Success)
103-
{
104-
authRequest.Realm = headerValue;
105-
}
106-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_NONCE_KEY + "$", RegexOptions.IgnoreCase).Success)
107-
{
108-
authRequest.Nonce = headerValue;
109-
}
110-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_USERNAME_KEY + "$", RegexOptions.IgnoreCase).Success)
111-
{
112-
authRequest.Username = headerValue;
113-
}
114-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_RESPONSE_KEY + "$", RegexOptions.IgnoreCase).Success)
115-
{
116-
authRequest.Response = headerValue;
117-
}
118-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_URI_KEY + "$", RegexOptions.IgnoreCase).Success)
154+
static string BuildQop(ReadOnlySpan<char> headerValue)
119155
{
120-
authRequest.URI = headerValue;
156+
Span<char> chars = stackalloc char[headerValue.Length];
157+
_ = headerValue.ToLowerInvariant(chars);
158+
return new string((ReadOnlySpan<char>)chars);
121159
}
122-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_CNONCE_KEY + "$", RegexOptions.IgnoreCase).Success)
123-
{
124-
authRequest.Cnonce = headerValue;
125-
}
126-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_NONCECOUNT_KEY + "$", RegexOptions.IgnoreCase).Success)
127-
{
128-
Int32.TryParse(headerValue, out authRequest.NonceCount);
129-
}
130-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_QOP_KEY + "$", RegexOptions.IgnoreCase).Success)
131-
{
132-
authRequest.Qop = headerValue.ToLower();
133-
}
134-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_OPAQUE_KEY + "$", RegexOptions.IgnoreCase).Success)
160+
#else
161+
authRequest.Qop = headerValue.ToString().ToLowerInvariant();
162+
#endif
163+
}
164+
else if (headerName.Equals(AuthHeaders.AUTH_OPAQUE_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
165+
{
166+
authRequest.Opaque = headerValue.ToString();
167+
}
168+
else if (headerName.Equals(AuthHeaders.AUTH_ALGORITHM_KEY.AsSpan(), StringComparison.OrdinalIgnoreCase))
169+
{
170+
//authRequest.Algorithm = headerValue;
171+
172+
#if NETCOREAPP6_0_OR_GREATER
173+
var hv = headerValue.Replace("-", "");
174+
#else
175+
var hv = headerValue.ToString().Replace("-", "");
176+
#endif
177+
if (Enum.TryParse<DigestAlgorithmsEnum>(hv, true, out var alg))
135178
{
136-
authRequest.Opaque = headerValue;
179+
authRequest.DigestAlgorithm = alg;
137180
}
138-
else if (Regex.Match(headerName, "^" + AuthHeaders.AUTH_ALGORITHM_KEY + "$", RegexOptions.IgnoreCase).Success)
181+
else
139182
{
140-
//authRequest.Algorithhm = headerValue;
141-
142-
if (Enum.TryParse<DigestAlgorithmsEnum>(headerValue.Replace("-",""), true, out var alg))
143-
{
144-
authRequest.DigestAlgorithm = alg;
145-
}
146-
else
147-
{
148-
logger.LogWarning("SIPAuthorisationDigest did not recognised digest algorithm value of {DigestAlgorithms}, defaulting to {DigestAlgorithmsEnumMD5}.", headerValue, DigestAlgorithmsEnum.MD5);
149-
authRequest.DigestAlgorithm = DigestAlgorithmsEnum.MD5;
150-
}
183+
logger.LogWarning("SIPAuthorisationDigest did not recognised digest algorithm value of {DigestAlgorithms}, defaulting to {DigestAlgorithmsEnumMD5}.", headerValue.ToString(), DigestAlgorithmsEnum.MD5);
184+
authRequest.DigestAlgorithm = DigestAlgorithmsEnum.MD5;
151185
}
152186
}
153187
}

test/unit/core/SIP/SIPAuthorisationDigestUnitTest.cs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,5 +433,109 @@ public void SIPRequestAuthRoundTripWithSHA256DIgest()
433433

434434
Assert.True(authResult.Authenticated);
435435
}
436-
}
436+
437+
private const string BASIC_AUTH_HEADER = "Digest realm=\"sip.domain.com\",nonce=\"1234\",algorithm=MD5";
438+
private const string FULL_AUTH_HEADER = "Digest username=\"alice\",realm=\"sip.domain.com\",nonce=\"1234abcd\",uri=\"sip:[email protected]\",response=\"a1b2c3d4\",algorithm=MD5,cnonce=\"9876\",opaque=\"opaque123\",qop=auth,nc=00000001";
439+
private const string SHA256_AUTH_HEADER = "Digest username=\"bob\",realm=\"sip.domain.com\",nonce=\"5678\",algorithm=SHA-256";
440+
private const string MALFORMED_AUTH_HEADER = "Digest realm=missing-quotes,nonce=\"1234\"";
441+
private const string MIXED_CASE_AUTH_HEADER = "DIgEsT rEaLm=\"sip.domain.com\",NoNcE=\"1234\",algorithm=md5";
442+
443+
[Theory]
444+
[InlineData(SIPAuthorisationHeadersEnum.WWWAuthenticate, BASIC_AUTH_HEADER, "sip.domain.com", "1234", DigestAlgorithmsEnum.MD5)]
445+
[InlineData(SIPAuthorisationHeadersEnum.ProxyAuthenticate, BASIC_AUTH_HEADER, "sip.domain.com", "1234", DigestAlgorithmsEnum.MD5)]
446+
public void BasicAuthHeaderParseTest(SIPAuthorisationHeadersEnum authType, string header, string expectedRealm, string expectedNonce, DigestAlgorithmsEnum expectedAlgorithm)
447+
{
448+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(authType, header);
449+
450+
Assert.Equal(authType, result.AuthorisationType);
451+
Assert.Equal(expectedRealm, result.Realm);
452+
Assert.Equal(expectedNonce, result.Nonce);
453+
Assert.Equal(expectedAlgorithm, result.DigestAlgorithm);
454+
}
455+
456+
[Theory]
457+
[InlineData(SIPAuthorisationHeadersEnum.Authorize, FULL_AUTH_HEADER)]
458+
public void FullAuthHeaderParseTest(SIPAuthorisationHeadersEnum authType, string header)
459+
{
460+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(authType, header);
461+
462+
Assert.Equal("alice", result.Username);
463+
Assert.Equal("sip.domain.com", result.Realm);
464+
Assert.Equal("1234abcd", result.Nonce);
465+
Assert.Equal("sip:[email protected]", result.URI);
466+
Assert.Equal("a1b2c3d4", result.Response);
467+
Assert.Equal(DigestAlgorithmsEnum.MD5, result.DigestAlgorithm);
468+
Assert.Equal("9876", result.Cnonce);
469+
Assert.Equal("opaque123", result.Opaque);
470+
Assert.Equal("auth", result.Qop);
471+
Assert.Equal(1, result.NonceCount);
472+
}
473+
474+
[Theory]
475+
[InlineData(SIPAuthorisationHeadersEnum.ProxyAuthenticate, SHA256_AUTH_HEADER, DigestAlgorithmsEnum.SHA256)]
476+
public void AlgorithmParseTest(SIPAuthorisationHeadersEnum authType, string header, DigestAlgorithmsEnum expectedAlgorithm)
477+
{
478+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(authType, header);
479+
Assert.Equal(expectedAlgorithm, result.DigestAlgorithm);
480+
}
481+
482+
[Theory]
483+
[InlineData(SIPAuthorisationHeadersEnum.WWWAuthenticate, MIXED_CASE_AUTH_HEADER)]
484+
public void CaseInsensitiveParseTest(SIPAuthorisationHeadersEnum authType, string header)
485+
{
486+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(authType, header);
487+
488+
Assert.Equal("sip.domain.com", result.Realm);
489+
Assert.Equal("1234", result.Nonce);
490+
Assert.Equal(DigestAlgorithmsEnum.MD5, result.DigestAlgorithm);
491+
}
492+
493+
[Theory]
494+
[InlineData(SIPAuthorisationHeadersEnum.WWWAuthenticate, "")]
495+
[InlineData(SIPAuthorisationHeadersEnum.WWWAuthenticate, "NotDigest realm=\"test\"")]
496+
public void EmptyOrInvalidHeaderTest(SIPAuthorisationHeadersEnum authType, string header)
497+
{
498+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(authType, header);
499+
500+
Assert.NotNull(result);
501+
Assert.Equal(authType, result.AuthorisationType);
502+
Assert.Null(result.Realm);
503+
Assert.Null(result.Nonce);
504+
}
505+
506+
[Theory]
507+
[InlineData("Digest nc=1", 1)]
508+
[InlineData("Digest nc=00000001", 1)]
509+
[InlineData("Digest nc=invalid", 0)]
510+
[InlineData("Digest nc=", 0)]
511+
public void NonceCountParseTest(string header, int expectedCount)
512+
{
513+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.Authorize, header);
514+
Assert.Equal(expectedCount, result.NonceCount);
515+
}
516+
517+
[Theory]
518+
[InlineData("Digest qop=auth", "auth")]
519+
[InlineData("Digest qop=AUTH", "auth")]
520+
[InlineData("Digest qop=Auth", "auth")]
521+
public void QopParseTest(string header, string expectedQop)
522+
{
523+
var result = SIPAuthorisationDigest.ParseAuthorisationDigest(SIPAuthorisationHeadersEnum.Authorize, header);
524+
Assert.Equal(expectedQop, result.Qop);
525+
}
526+
527+
[Fact]
528+
public void ParseAuthorisationDigest_InvalidAlgorithm_DefaultsToMD5()
529+
{
530+
// Arrange
531+
string authorisationRequest = "Digest username=\"user\", realm=\"realm\", nonce=\"nonce\", uri=\"uri\", response=\"response\", algorithm=\"invalid-algorithm\"";
532+
SIPAuthorisationHeadersEnum authorisationType = SIPAuthorisationHeadersEnum.Authorize;
533+
534+
// Act
535+
SIPAuthorisationDigest authDigest = SIPAuthorisationDigest.ParseAuthorisationDigest(authorisationType, authorisationRequest);
536+
537+
// Assert
538+
Assert.Equal(DigestAlgorithmsEnum.MD5, authDigest.DigestAlgorithm);
539+
}
540+
}
437541
}

0 commit comments

Comments
 (0)