Skip to content

Commit ef1e6e8

Browse files
authored
Refactor SIP parsing (#1645)
Modernize SIP message parsing and formatting for performance: - Replace string concat/Regex with Span-based parsing - Use StringBuilder and interpolated strings in ToString() - Improve header/parameter parsing, folded header handling - Update code style, error messages, and logging - Add Polyfill, set C# 14, ensure .NET 4.6.2 compatibility - Remove obsolete code, update project metadata and docs
1 parent 289b160 commit ef1e6e8

25 files changed

Lines changed: 998 additions & 544 deletions

src/SIPSorcery/SIPSorcery.csproj

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
2323
<PackageReference Include="DnsClient" Version="1.8.0" />
2424
<PackageReference Include="Makaretu.Dns.Multicast" Version="0.27.0" />
25+
<PackageReference Include="Polyfill" Version="10.7.0">
26+
<PrivateAssets>all</PrivateAssets>
27+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28+
</PackageReference>
2529
<PackageReference Include="SIPSorcery.WebSocketSharp" Version="0.0.1" />
2630
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
2731
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.203" PrivateAssets="All" />
@@ -35,13 +39,17 @@
3539
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
3640
</ItemGroup>
3741

42+
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
43+
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
44+
</ItemGroup>
45+
3846
<ItemGroup>
3947
<ProjectReference Include="..\SIPSorceryMedia.Abstractions\SIPSorceryMedia.Abstractions.csproj" />
4048
</ItemGroup>
4149

4250
<PropertyGroup>
4351
<TargetFrameworks>netstandard2.0;netstandard2.1;netcoreapp3.1;net462;net5.0;net6.0;net8.0;net9.0;net10.0</TargetFrameworks>
44-
<LangVersion>latest</LangVersion>
52+
<LangVersion>14.0</LangVersion>
4553
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
4654
<NoWarn>$(NoWarn);SYSLIB0050</NoWarn>
4755
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
@@ -66,11 +74,12 @@
6674
<RepositoryUrl>https://github.com/sipsorcery-org/sipsorcery</RepositoryUrl>
6775
<RepositoryType>git</RepositoryType>
6876
<RepositoryBranch>master</RepositoryBranch>
77+
<PolyArgumentExceptions>true</PolyArgumentExceptions>
6978
<PackageTags>SIP WebRTC VoIP RTP SDP STUN ICE SIPSorcery</PackageTags>
7079
<PackageReleaseNotes>-v10.0.8: Bug fixes.
7180
-v10.0.7: Network address change fix for Unity.
72-
-v10.0.6: Bug fixes.
73-
-v10.0.5: Stable release. Bug fixes.
81+
-v10.0.6: Bug fixes.
82+
-v10.0.5: Stable release. Bug fixes.
7483
-v10.0.4-pre: New SRTP and DTLS implementation (huge thanks to @jimm98y).
7584
-v10.0.3: Removed null SRTP ciphers.
7685
-v10.0.2: Removed use of master key index for SRTP.
@@ -94,7 +103,7 @@
94103
-v8.0.0: RTP header extension improvements (thanks to @ChristopheI). Major version to 8 to reflect highest .net runtime supported.</PackageReleaseNotes>
95104
<NeutralLanguage>en</NeutralLanguage>
96105
<Version>10.0.8</Version>
97-
<AssemblyVersion>10.0.8</AssemblyVersion>
106+
<AssemblyVersion>10.0.8</AssemblyVersion>
98107
<FileVersion>10.0.8</FileVersion>
99108
</PropertyGroup>
100109

src/SIPSorcery/app/Media/Sources/AudioExtrasSource.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,7 @@ public int AudioSamplePeriodMilliseconds
159159
{
160160
if (value < AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN || value > AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX)
161161
{
162-
throw new ApplicationException("Invalid value for the audio sample period. Must be between " +
163-
$"{AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN} and {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX}ms.");
162+
throw new ApplicationException($"Invalid value for the audio sample period. Must be between {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MIN} and {AUDIO_SAMPLE_PERIOD_MILLISECONDS_MAX}ms.");
164163
}
165164
else
166165
{

src/SIPSorcery/app/SIPPacketMangler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static string MangleSDP(string sdpBody, string publicIPAddress, out bool
5555
&& pubaddr.AddressFamily == AddressFamily.InterNetworkV6
5656
&& addr.AddressFamily == AddressFamily.InterNetworkV6)
5757
{
58-
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP6 (?<ipaddress>([:a-fA-F0-9]+))", "c=IN IP6" + publicIPAddress, RegexOptions.Singleline);
58+
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP6 (?<ipaddress>([:a-fA-F0-9]+))", $"c=IN IP6{publicIPAddress}", RegexOptions.Singleline);
5959
wasMangled = true;
6060

6161
return mangledSDP;
@@ -65,7 +65,7 @@ public static string MangleSDP(string sdpBody, string publicIPAddress, out bool
6565
&& addr.AddressFamily == AddressFamily.InterNetwork)
6666
{
6767
//logger.LogDebug("MangleSDP replacing private " + sdpAddress + " with " + publicIPAddress + ".");
68-
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP4 (?<ipaddress>(\d+\.){3}\d+)", "c=IN IP4 " + publicIPAddress, RegexOptions.Singleline);
68+
string mangledSDP = Regex.Replace(sdpBody, @"c=IN IP4 (?<ipaddress>(\d+\.){3}\d+)", $"c=IN IP4 {publicIPAddress}", RegexOptions.Singleline);
6969
wasMangled = true;
7070

7171
return mangledSDP;

src/SIPSorcery/app/SIPUserAgents/SIPCallDescriptor.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public class SIPCallDescriptor
160160
public SIPCallDescriptor(ISIPAccount toSIPAccount, string uri, string fromHeader, string contentType, string content)
161161
{
162162
ToSIPAccount = toSIPAccount;
163-
Uri = uri ?? toSIPAccount.SIPUsername + "@" + toSIPAccount.SIPDomain;
163+
Uri = uri ?? $"{toSIPAccount.SIPUsername}@{toSIPAccount.SIPDomain}";
164164
From = fromHeader;
165165
ContentType = contentType;
166166
Content = content;
@@ -291,14 +291,14 @@ public void ParseCallOptions(string options)
291291
options = options.Trim('[', ']');
292292

293293
// Parse delay time option.
294-
Match delayCallMatch = Regex.Match(options, DELAY_CALL_OPTION_KEY + @"=(?<delaytime>\d+)");
294+
Match delayCallMatch = Regex.Match(options, $@"{DELAY_CALL_OPTION_KEY}=(?<delaytime>\d+)");
295295
if (delayCallMatch.Success)
296296
{
297297
int.TryParse(delayCallMatch.Result("${delaytime}"), out DelaySeconds);
298298
}
299299

300300
// Parse redirect mode option.
301-
Match redirectModeMatch = Regex.Match(options, REDIRECT_MODE_OPTION_KEY + @"=(?<redirectmode>\w)");
301+
Match redirectModeMatch = Regex.Match(options, $@"{REDIRECT_MODE_OPTION_KEY}=(?<redirectmode>\w)");
302302
if (redirectModeMatch.Success)
303303
{
304304
string redirectMode = redirectModeMatch.Result("${redirectmode}");
@@ -321,42 +321,42 @@ public void ParseCallOptions(string options)
321321
}
322322

323323
// Parse call duration limit option.
324-
Match callDurationMatch = Regex.Match(options, CALL_DURATION_OPTION_KEY + @"=(?<callduration>\d+)");
324+
Match callDurationMatch = Regex.Match(options, $@"{CALL_DURATION_OPTION_KEY}=(?<callduration>\d+)");
325325
if (callDurationMatch.Success)
326326
{
327327
int.TryParse(callDurationMatch.Result("${callduration}"), out CallDurationLimit);
328328
}
329329

330330
// Parse the mangle option.
331-
Match mangleMatch = Regex.Match(options, MANGLE_MODE_OPTION_KEY + @"=(?<mangle>\w+)");
331+
Match mangleMatch = Regex.Match(options, $@"{MANGLE_MODE_OPTION_KEY}=(?<mangle>\w+)");
332332
if (mangleMatch.Success)
333333
{
334334
bool.TryParse(mangleMatch.Result("${mangle}"), out MangleResponseSDP);
335335
}
336336

337337
// Parse the From header display name option.
338-
Match fromDisplayNameMatch = Regex.Match(options, FROM_DISPLAY_NAME_KEY + @"=(?<displayname>.+?)(,|$)");
338+
Match fromDisplayNameMatch = Regex.Match(options, $@"{FROM_DISPLAY_NAME_KEY}=(?<displayname>.+?)(,|$)");
339339
if (fromDisplayNameMatch.Success)
340340
{
341341
FromDisplayName = fromDisplayNameMatch.Result("${displayname}").Trim();
342342
}
343343

344344
// Parse the From header URI username option.
345-
Match fromUsernameNameMatch = Regex.Match(options, FROM_USERNAME_KEY + @"=(?<username>.+?)(,|$)");
345+
Match fromUsernameNameMatch = Regex.Match(options, $@"{FROM_USERNAME_KEY}=(?<username>.+?)(,|$)");
346346
if (fromUsernameNameMatch.Success)
347347
{
348348
FromURIUsername = fromUsernameNameMatch.Result("${username}").Trim();
349349
}
350350

351351
// Parse the From header URI host option.
352-
Match fromURIHostMatch = Regex.Match(options, FROM_HOST_KEY + @"=(?<host>.+?)(,|$)");
352+
Match fromURIHostMatch = Regex.Match(options, $@"{FROM_HOST_KEY}=(?<host>.+?)(,|$)");
353353
if (fromURIHostMatch.Success)
354354
{
355355
FromURIHost = fromURIHostMatch.Result("${host}").Trim();
356356
}
357357

358358
// Parse the Transfer behaviour option.
359-
Match transferMatch = Regex.Match(options, TRANSFER_MODE_OPTION_KEY + @"=(?<transfermode>.+?)(,|$)");
359+
Match transferMatch = Regex.Match(options, $@"{TRANSFER_MODE_OPTION_KEY}=(?<transfermode>.+?)(,|$)");
360360
if (transferMatch.Success)
361361
{
362362
string transferMode = transferMatch.Result("${transfermode}");
@@ -387,28 +387,28 @@ public void ParseCallOptions(string options)
387387
}
388388

389389
// Parse the request caller details option.
390-
Match callerDetailsMatch = Regex.Match(options, REQUEST_CALLER_DETAILS + @"=(?<callerdetails>\w+)");
390+
Match callerDetailsMatch = Regex.Match(options, $@"{REQUEST_CALLER_DETAILS}=(?<callerdetails>\w+)");
391391
if (callerDetailsMatch.Success)
392392
{
393393
bool.TryParse(callerDetailsMatch.Result("${callerdetails}"), out RequestCallerDetails);
394394
}
395395

396396
// Parse the accountcode.
397-
Match accountCodeMatch = Regex.Match(options, ACCOUNT_CODE_KEY + @"=(?<accountCode>\w+)");
397+
Match accountCodeMatch = Regex.Match(options, $@"{ACCOUNT_CODE_KEY}=(?<accountCode>\w+)");
398398
if (accountCodeMatch.Success)
399399
{
400400
AccountCode = accountCodeMatch.Result("${accountCode}");
401401
}
402402

403403
// Parse the rate code.
404-
Match rateCodeMatch = Regex.Match(options, RATE_CODE_KEY + @"=(?<rateCode>\w+)");
404+
Match rateCodeMatch = Regex.Match(options, $@"{RATE_CODE_KEY}=(?<rateCode>\w+)");
405405
if (rateCodeMatch.Success)
406406
{
407407
RateCode = rateCodeMatch.Result("${rateCode}");
408408
}
409409

410410
// Parse the delayed reinvite option.
411-
Match delayedReinviteMatch = Regex.Match(options, DELAYED_REINVITE_KEY + @"=(?<delayedReinvite>\d+)");
411+
Match delayedReinviteMatch = Regex.Match(options, $@"{DELAYED_REINVITE_KEY}=(?<delayedReinvite>\d+)");
412412
if (delayedReinviteMatch.Success)
413413
{
414414
int.TryParse(delayedReinviteMatch.Result("${delayedReinvite}"), out ReinviteDelay);
@@ -457,7 +457,14 @@ public static List<string> ParseCustomHeaders(string customHeaders)
457457
//string headerName = customHeader.Substring(0, colonIndex).Trim();
458458
//string headerValue = (customHeader.Length > colonIndex) ? customHeader.Substring(colonIndex + 1).Trim() : String.Empty;
459459

460-
if (Regex.Match(customHeader.Trim(), "^(Via|From|Contact|CSeq|Call-ID|Max-Forwards|Content-Length)$", RegexOptions.IgnoreCase).Success)
460+
var trimmedCustomHeader = customHeader.AsSpan().Trim();
461+
if (trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_VIA, StringComparison.OrdinalIgnoreCase) ||
462+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_FROM, StringComparison.OrdinalIgnoreCase) ||
463+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTACT, StringComparison.OrdinalIgnoreCase) ||
464+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CSEQ, StringComparison.OrdinalIgnoreCase) ||
465+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CALLID, StringComparison.OrdinalIgnoreCase) ||
466+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_MAXFORWARDS, StringComparison.OrdinalIgnoreCase) ||
467+
trimmedCustomHeader.Equals(SIPHeaders.SIP_HEADER_CONTENTLENGTH, StringComparison.OrdinalIgnoreCase))
461468
{
462469
logger.LogWarning("ParseCustomHeaders skipping custom header due to an non-permitted string in header name, {CustomHeader}.", customHeader);
463470
continue;

src/SIPSorcery/app/SIPUserAgents/SIPClientUserAgent.cs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ public void Cancel(string reason = null)
286286
// If auth header is included inside INVITE request, we re-include them inside CANCEL request
287287
if (m_serverTransaction.TransactionRequest.Header.HasAuthenticationHeader)
288288
{
289-
string username = (m_sipCallDescriptor.AuthUsername == null || m_sipCallDescriptor.AuthUsername.Trim().Length <= 0 ? m_sipCallDescriptor.Username : m_sipCallDescriptor.AuthUsername);
289+
var username = string.IsNullOrWhiteSpace(m_sipCallDescriptor.AuthUsername) ? m_sipCallDescriptor.Username : m_sipCallDescriptor.AuthUsername;
290290
SIPAuthorisationDigest authDigest = m_serverTransaction.TransactionRequest.Header.AuthenticationHeaders.First().SIPDigest;
291291
authDigest.SetCredentials(username, m_sipCallDescriptor.Password, m_sipCallDescriptor.Uri, SIPMethodsEnum.CANCEL.ToString());
292292

@@ -332,7 +332,7 @@ public void Hangup(string reason = null)
332332
//SIPRequest byeRequest = GetByeRequest(m_serverTransaction.TransactionFinalResponse, m_sipDialogue.RemoteTarget);
333333
SIPRequest byeRequest = m_sipDialogue.GetInDialogRequest(SIPMethodsEnum.BYE);
334334
byeRequest.SetSendFromHints(m_serverTransaction.TransactionRequest.LocalSIPEndPoint);
335-
335+
336336
if (!string.IsNullOrEmpty(reason))
337337
{
338338
// The REASON header gets pre-appended automatically in the SIPHeader class as "Reason: " when ToString() is called on the SIP Header class.
@@ -427,7 +427,7 @@ private Task<SocketError> ServerFinalResponseReceived(SIPEndPoint localSIPEndPoi
427427
m_serverAuthAttempts = 1;
428428

429429
// Resend INVITE with credentials.
430-
string username = (m_sipCallDescriptor.AuthUsername != null && m_sipCallDescriptor.AuthUsername.Trim().Length > 0) ? m_sipCallDescriptor.AuthUsername : m_sipCallDescriptor.Username;
430+
var username = !string.IsNullOrWhiteSpace(m_sipCallDescriptor.AuthUsername) ? m_sipCallDescriptor.AuthUsername : m_sipCallDescriptor.Username;
431431
var authRequest = m_serverTransaction.TransactionRequest.DuplicateAndAuthenticate(sipResponse.Header.AuthenticationHeaders,
432432
username, m_sipCallDescriptor.Password);
433433

@@ -529,7 +529,7 @@ private Task<SocketError> ByeServerFinalResponseReceived(SIPEndPoint localSIPEnd
529529

530530
if (sipResponse.Status == SIPResponseStatusCodesEnum.ProxyAuthenticationRequired || sipResponse.Status == SIPResponseStatusCodesEnum.Unauthorised)
531531
{
532-
string username = (m_sipCallDescriptor.AuthUsername == null || m_sipCallDescriptor.AuthUsername.Trim().Length <= 0 ? m_sipCallDescriptor.Username : m_sipCallDescriptor.AuthUsername);
532+
var username = string.IsNullOrWhiteSpace(m_sipCallDescriptor.AuthUsername) ? m_sipCallDescriptor.Username : m_sipCallDescriptor.AuthUsername;
533533
var authRequest = transaction.TransactionRequest.DuplicateAndAuthenticate(sipResponse.Header.AuthenticationHeaders,
534534
username, m_sipCallDescriptor.Password);
535535

@@ -560,8 +560,9 @@ private SIPRequest GetInviteRequest(SIPCallDescriptor sipCallDescriptor, string
560560
inviteHeader.CSeqMethod = SIPMethodsEnum.INVITE;
561561
inviteHeader.UserAgent = SIPConstants.SipUserAgentVersionString;
562562
inviteHeader.Routes = routeSet;
563-
inviteHeader.Supported = SIPExtensionHeaders.REPLACES + ", " + SIPExtensionHeaders.NO_REFER_SUB
564-
+ ((PrackSupported == true) ? ", " + SIPExtensionHeaders.PRACK : "");
563+
inviteHeader.Supported = PrackSupported == true
564+
? $"{SIPExtensionHeaders.REPLACES}, {SIPExtensionHeaders.NO_REFER_SUB}, {SIPExtensionHeaders.PRACK}"
565+
: $"{SIPExtensionHeaders.REPLACES}, {SIPExtensionHeaders.NO_REFER_SUB}";
565566

566567
inviteRequest.Header = inviteHeader;
567568

@@ -587,13 +588,17 @@ private SIPRequest GetInviteRequest(SIPCallDescriptor sipCallDescriptor, string
587588
{
588589
continue;
589590
}
590-
else if (customHeader.Trim().StartsWith(SIPHeaders.SIP_HEADER_USERAGENT))
591+
592+
var customHeaderSpan = customHeader.AsSpan().Trim();
593+
if (customHeaderSpan.StartsWith(SIPHeaders.SIP_HEADER_USERAGENT, StringComparison.Ordinal))
591594
{
592-
inviteRequest.Header.UserAgent = customHeader.Substring(customHeader.IndexOf(":") + 1).Trim();
595+
inviteRequest.Header.UserAgent = customHeader.Substring(customHeader.IndexOf(':') + 1).Trim();
593596
}
594-
else if (customHeader.Trim().StartsWith(SIPHeaders.SIP_HEADER_TO + ":"))
597+
else if (customHeaderSpan.StartsWith(SIPHeaders.SIP_HEADER_TO, StringComparison.Ordinal) &&
598+
customHeaderSpan.Length > SIPHeaders.SIP_HEADER_TO.Length &&
599+
customHeaderSpan[SIPHeaders.SIP_HEADER_TO.Length] == ':')
595600
{
596-
var customToHeader = SIPUserField.ParseSIPUserField(customHeader.Substring(customHeader.IndexOf(":") + 1).Trim());
601+
var customToHeader = SIPUserField.ParseSIPUserField(customHeader.Substring(customHeader.IndexOf(':') + 1).Trim());
597602
if (customToHeader != null)
598603
{
599604
inviteRequest.Header.To.ToUserField = customToHeader;

src/SIPSorcery/app/SIPUserAgents/SIPNonInviteClientUserAgent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private SIPRequest GetRequest(SIPMethodsEnum method)
159159
{
160160
continue;
161161
}
162-
else if (customHeader.Trim().StartsWith(SIPHeaders.SIP_HEADER_USERAGENT))
162+
else if (customHeader.AsSpan().Trim().StartsWith(SIPHeaders.SIP_HEADER_USERAGENT, StringComparison.Ordinal))
163163
{
164164
request.Header.UserAgent = customHeader.Substring(customHeader.IndexOf(":") + 1).Trim();
165165
}

0 commit comments

Comments
 (0)