diff --git a/.gitignore b/.gitignore
index 57907c574..7dd56f4a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -235,3 +235,4 @@ html/
# sonar cloud stuff
.sonarqube
+/test/Twilio.Benchmark/BenchmarkDotNet.Artifacts
diff --git a/Twilio.sln b/Twilio.sln
index f75efdaab..f11fa4220 100644
--- a/Twilio.sln
+++ b/Twilio.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26206.0
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{36585F38-8C30-49A9-BDA1-9A0DC61C288B}"
EndProject
@@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilio", "src\Twilio\Twilio
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilio.Test", "test\Twilio.Test\Twilio.Test.csproj", "{DC35107A-F987-47A3-B0BC-7110BA15943C}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilio.Benchmark", "test\Twilio.Benchmark\Twilio.Benchmark.csproj", "{80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,18 @@ Global
{DC35107A-F987-47A3-B0BC-7110BA15943C}.Release|x64.Build.0 = Release|Any CPU
{DC35107A-F987-47A3-B0BC-7110BA15943C}.Release|x86.ActiveCfg = Release|Any CPU
{DC35107A-F987-47A3-B0BC-7110BA15943C}.Release|x86.Build.0 = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|x64.Build.0 = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Debug|x86.Build.0 = Debug|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|x64.ActiveCfg = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|x64.Build.0 = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|x86.ActiveCfg = Release|Any CPU
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -52,6 +66,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{62BB8FE9-99DD-475D-80EB-D2E53C380754} = {36585F38-8C30-49A9-BDA1-9A0DC61C288B}
{DC35107A-F987-47A3-B0BC-7110BA15943C} = {FE04FB2E-73FB-4E45-AAEE-EE04754A5E9C}
+ {80DEE3E3-5F8E-40C2-A68C-15463C4CFAB1} = {FE04FB2E-73FB-4E45-AAEE-EE04754A5E9C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {75638FC3-0E0B-4D79-8BEB-8CC499BF98C5}
diff --git a/src/Twilio/Security/RequestValidator.cs b/src/Twilio/Security/RequestValidator.cs
index aa94ada48..8a1dcca1c 100644
--- a/src/Twilio/Security/RequestValidator.cs
+++ b/src/Twilio/Security/RequestValidator.cs
@@ -1,164 +1,233 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Security.Cryptography;
-using System.Text;
-
-namespace Twilio.Security
-{
- ///
- /// Twilio request validator
- ///
- public class RequestValidator
- {
- private readonly HMACSHA1 _hmac;
- private readonly SHA256 _sha;
-
- ///
- /// Create a new RequestValidator
- ///
- /// Signing secret
- public RequestValidator(string secret)
- {
- _hmac = new HMACSHA1(Encoding.UTF8.GetBytes(secret));
- _sha = SHA256.Create();
- }
-
- ///
- /// Validate against a request
- ///
- /// Request URL
- /// Request parameters
- /// Expected result
- /// true if the signature matches the result; false otherwise
- public bool Validate(string url, NameValueCollection parameters, string expected)
- {
- return Validate(url, ToDictionary(parameters), expected);
- }
-
- ///
- /// Validate against a request
- ///
- /// Request URL
- /// Request parameters
- /// Expected result
- /// true if the signature matches the result; false otherwise
- public bool Validate(string url, IDictionary parameters, string expected)
- {
- // check signature of url with and without port, since sig generation on back end is inconsistent
- var signatureWithoutPort = GetValidationSignature(RemovePort(url), parameters);
- var signatureWithPort = GetValidationSignature(AddPort(url), parameters);
- // If either url produces a valid signature, we accept the request as valid
- return SecureCompare(signatureWithoutPort, expected) || SecureCompare(signatureWithPort, expected);
- }
-
- public bool Validate(string url, string body, string expected)
- {
- var paramString = new UriBuilder(url).Query.TrimStart('?');
- var bodyHash = "";
- foreach (var param in paramString.Split('&'))
- {
- var split = param.Split('=');
- if (split[0] == "bodySHA256")
- {
- bodyHash = Uri.UnescapeDataString(split[1]);
- }
- }
-
- return Validate(url, new Dictionary(), expected) && ValidateBody(body, bodyHash);
- }
-
- public bool ValidateBody(string rawBody, string expected)
- {
- var signature = _sha.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
- return SecureCompare(BitConverter.ToString(signature).Replace("-","").ToLower(), expected);
- }
-
- private static IDictionary ToDictionary(NameValueCollection col)
- {
- var dict = new Dictionary();
- foreach (var k in col.AllKeys)
- {
- dict.Add(k, col[k]);
- }
- return dict;
- }
-
- private string GetValidationSignature(string url, IDictionary parameters)
- {
- var b = new StringBuilder(url);
- if (parameters != null)
- {
- var sortedKeys = new List(parameters.Keys);
- sortedKeys.Sort(StringComparer.Ordinal);
-
- foreach (var key in sortedKeys)
- {
- b.Append(key).Append(parameters[key] ?? "");
- }
- }
-
- var hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(b.ToString()));
- return Convert.ToBase64String(hash);
- }
-
- private static bool SecureCompare(string a, string b)
- {
- if (a == null || b == null)
- {
- return false;
- }
-
- var n = a.Length;
- if (n != b.Length)
- {
- return false;
- }
-
- var mismatch = 0;
- for (var i = 0; i < n; i++)
- {
- mismatch |= a[i] ^ b[i];
- }
-
- return mismatch == 0;
- }
-
- private string RemovePort(string url)
- {
- return SetPort(url, -1);
- }
-
- private string AddPort(string url)
- {
- var uri = new UriBuilder(url);
- return SetPort(url, uri.Port);
- }
-
- private string SetPort(string url, int port)
- {
- var uri = new UriBuilder(url);
- uri.Host = PreserveCase(url, uri.Host);
- if (port == -1)
- {
- uri.Port = port;
- }
- else if ((port != 443) && (port != 80))
- {
- uri.Port = port;
- }
- else
- {
- uri.Port = uri.Scheme == "https" ? 443 : 80;
- }
- var scheme = PreserveCase(url, uri.Scheme);
- return uri.Uri.OriginalString.Replace(uri.Scheme, scheme);
- }
-
- private string PreserveCase(string url, string replacementString)
- {
- var startIndex = url.IndexOf(replacementString, StringComparison.OrdinalIgnoreCase);
- return url.Substring(startIndex, replacementString.Length);
- }
- }
-}
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Security.Cryptography;
+using System.Text;
+using System.Runtime.CompilerServices;
+
+namespace Twilio.Security
+{
+ ///
+ /// Twilio request validator
+ ///
+ public class RequestValidator
+ {
+ private readonly byte[] _secret;
+
+ ///
+ /// Create a new RequestValidator
+ ///
+ /// Signing secret
+ public RequestValidator(string secret)
+ {
+ _secret = Encoding.UTF8.GetBytes(secret);
+ }
+
+ ///
+ /// Validate against a request
+ ///
+ /// Request URL
+ /// Request parameters
+ /// Expected result
+ /// true if the signature matches the result; false otherwise
+ public bool Validate(string url, NameValueCollection parameters, string expected)
+ {
+ return Validate(url, ToDictionary(parameters), expected);
+ }
+
+ ///
+ /// Validate against a request
+ ///
+ /// Request URL
+ /// Request parameters
+ /// Expected result
+ /// true if the signature matches the result; false otherwise
+ public bool Validate(string url, IDictionary parameters, string expected)
+ {
+ if (string.IsNullOrEmpty(url))
+ throw new ArgumentException("Parameter 'url' cannot be null or empty.", nameof(url));
+ if (string.IsNullOrEmpty(expected))
+ throw new ArgumentException("Parameter 'expected' cannot be null or empty.", nameof(url));
+
+#if NET6_0_OR_GREATER
+ {
+ byte[] computeHash(byte[] buffer) => HMACSHA1.HashData(_secret, buffer);
+#else
+ using (var hmac = new HMACSHA1(_secret))
+ {
+ Func computeHash = hmac.ComputeHash;
+#endif
+ if (parameters == null || parameters.Count == 0)
+ {
+ var signature = GetValidationSignature(url, computeHash);
+ if (SecureCompare(signature, expected)) return true;
+
+ // check signature of url with and without port, since sig generation on back end is inconsistent
+ // If either url produces a valid signature, we accept the request as valid
+ url = GetUriVariation(url);
+ signature = GetValidationSignature(url, computeHash);
+ if (SecureCompare(signature, expected)) return true;
+ return false;
+ }
+ else
+ {
+ var parameterStringBuilder = GetJoinedParametersStringBuilder(parameters);
+ parameterStringBuilder.Insert(0, url);
+ var signature = GetValidationSignature(parameterStringBuilder.ToString(), computeHash);
+ if (SecureCompare(signature, expected)) return true;
+ parameterStringBuilder.Remove(0, url.Length);
+
+ // check signature of url with and without port, since sig generation on back end is inconsistent
+ // If either url produces a valid signature, we accept the request as valid
+ url = GetUriVariation(url);
+ parameterStringBuilder.Insert(0, url);
+ signature = GetValidationSignature(parameterStringBuilder.ToString(), computeHash);
+ if (SecureCompare(signature, expected)) return true;
+
+ return false;
+ }
+ }
+ }
+
+ private StringBuilder GetJoinedParametersStringBuilder(IDictionary parameters)
+ {
+ var keys = parameters.Keys.ToArray();
+ Array.Sort(keys, StringComparer.Ordinal);
+
+ var b = new StringBuilder();
+ foreach (var key in keys)
+ {
+ b.Append(key).Append(parameters[key] ?? "");
+ }
+
+ return b;
+ }
+
+ public bool Validate(string url, string body, string expected)
+ {
+ if (string.IsNullOrEmpty(url))
+ throw new ArgumentException("Parameter 'url' cannot be null or empty.", nameof(url));
+ if (string.IsNullOrEmpty(expected))
+ throw new ArgumentException("Parameter 'expected' cannot be null or empty.", nameof(expected));
+
+ var paramString = new Uri(url, UriKind.Absolute).Query.TrimStart('?');
+ var bodyHash = "";
+ foreach (var param in paramString.Split('&'))
+ {
+ var split = param.Split('=');
+ if (split[0] == "bodySHA256")
+ {
+ bodyHash = Uri.UnescapeDataString(split[1]);
+ }
+ }
+
+ return Validate(url, (IDictionary)null, expected) && ValidateBody(body, bodyHash);
+ }
+
+ public static bool ValidateBody(string rawBody, string expected)
+ {
+#if NET6_0_OR_GREATER
+ {
+ var signature = SHA256.HashData(Encoding.UTF8.GetBytes(rawBody));
+#else
+ using (var sha = SHA256.Create())
+ {
+ var signature = sha.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
+#endif
+ return SecureCompare(BitConverter.ToString(signature).Replace("-", "").ToLower(), expected);
+ }
+ }
+
+ private static IDictionary ToDictionary(NameValueCollection col)
+ {
+ var dict = new Dictionary();
+ foreach (var k in col.AllKeys)
+ {
+ dict.Add(k, col[k]);
+ }
+
+ return dict;
+ }
+
+ private string GetValidationSignature(string urlWithParameters, Func computeHash)
+ {
+ var hash = computeHash(Encoding.UTF8.GetBytes(urlWithParameters));
+ return Convert.ToBase64String(hash);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static bool SecureCompare(string a, string b)
+ {
+ if (a == null || b == null)
+ {
+ return false;
+ }
+
+ var n = a.Length;
+ if (n != b.Length)
+ {
+ return false;
+ }
+
+ var mismatch = 0;
+ for (var i = 0; i < n; i++)
+ {
+ mismatch |= a[i] ^ b[i];
+ }
+
+ return mismatch == 0;
+ }
+
+ ///
+ /// Returns URL without port if given URL has port, returns URL with port if given URL has no port
+ ///
+ ///
+ ///
+ private static string GetUriVariation(string url)
+ {
+ var uri = new Uri(url);
+ var uriBuilder = new UriBuilder(uri);
+ var port = uri.GetComponents(UriComponents.Port, UriFormat.UriEscaped);
+ // if port already removed
+ if (port == "")
+ {
+ return SetPort(url, uriBuilder, uriBuilder.Port);
+ }
+
+ return SetPort(url, uriBuilder, -1);
+ }
+
+ private static string SetPort(string url, UriBuilder uri, int newPort)
+ {
+ if (newPort == -1)
+ {
+ uri.Port = newPort;
+ }
+ else if (newPort != 443 && newPort != 80)
+ {
+ uri.Port = newPort;
+ }
+ else
+ {
+ uri.Port = uri.Scheme == "https" ? 443 : 80;
+ }
+
+ var uriStringBuilder = new StringBuilder(uri.ToString());
+
+ var host = PreserveCase(url, uri.Host);
+ uriStringBuilder.Replace(uri.Host, host);
+
+ var scheme = PreserveCase(url, uri.Scheme);
+ uriStringBuilder.Replace(uri.Scheme, scheme);
+
+ return uriStringBuilder.ToString();
+ }
+
+ private static string PreserveCase(string url, string replacementString)
+ {
+ var startIndex = url.IndexOf(replacementString, StringComparison.OrdinalIgnoreCase);
+ return url.Substring(startIndex, replacementString.Length);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Twilio/Twilio.csproj b/src/Twilio/Twilio.csproj
index 6d01d5bce..82e2476bc 100644
--- a/src/Twilio/Twilio.csproj
+++ b/src/Twilio/Twilio.csproj
@@ -1,59 +1,63 @@
-
- netstandard1.4;netstandard2.0;net451;net35
- true
- Twilio
- Twilio REST API helper library
- Copyright © Twilio
- Twilio
- en-US
- 6.16.1
-
-
- Twilio
- $(NoWarn);CS1591
- true
- true
- Twilio
- REST;SMS;voice;telephony;phone;twilio;twiml;video;wireless;api
- https://www.twilio.com/docs/static/company/img/logos/red/twilio-mark-red.898073bba.png
- http://github.com/twilio/twilio-csharp
- https://github.com/twilio/twilio-csharp/blob/HEAD/LICENSE
- http://github.com/twilio/twilio-csharp
- git
- 1.6.1
- 2.0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ net6.0;netstandard1.4;netstandard2.0;net451;net35
+ true
+ Twilio
+ Twilio REST API helper library
+ Copyright © Twilio
+ Twilio
+ en-US
+ 6.6.1
+ Twilio
+ $(NoWarn);CS1591
+ true
+ true
+ Twilio
+ REST;SMS;voice;telephony;phone;twilio;twiml;video;wireless;api
+ https://www.twilio.com/docs/static/company/img/logos/red/twilio-mark-red.898073bba.png
+ http://github.com/twilio/twilio-csharp
+ https://github.com/twilio/twilio-csharp/blob/HEAD/LICENSE
+ http://github.com/twilio/twilio-csharp
+ git
+ 1.6.1
+ 2.0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Twilio.Benchmark/Program.cs b/test/Twilio.Benchmark/Program.cs
new file mode 100644
index 000000000..8c8e2dcd2
--- /dev/null
+++ b/test/Twilio.Benchmark/Program.cs
@@ -0,0 +1,92 @@
+using System.Collections.Specialized;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using Twilio.Security;
+
+var summary = BenchmarkRunner.Run();
+Console.Write(summary);
+
+[MemoryDiagnoser]
+public class RequestValidationBenchmark
+{
+ private const string Secret = "12345";
+ private const string UnhappyPathUrl = "HTTP://MyCompany.com:8080/myapp.php?foo=1&bar=2";
+ private const string UnhappyPathSignature = "eYYN9fMlxrQMXOsr7bIzoPTrbxA=";
+ private const string HappyPathUrl = "https://mycompany.com/myapp.php?foo=1&bar=2";
+ private const string HappyPathSignature = "3LL3BFKOcn80artVM5inMPFpmtU=";
+ private static readonly NameValueCollection UnhappyPathParameters = new()
+ {
+ {"ToCountry", "US"},
+ {"ToState", "OH"},
+ {"SmsMessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"NumMedia", "0"},
+ {"ToCity", "UTICA"},
+ {"FromZip", "20705"},
+ {"SmsSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"FromState", "DC"},
+ {"SmsStatus", "received"},
+ {"FromCity", "BELTSVILLE"},
+ {"Body", "Ahoy!"},
+ {"FromCountry", "US"},
+ {"To", "+10123456789"},
+ {"ToZip", "43037"},
+ {"NumSegments", "1"},
+ {"ReferralNumMedia", "0"},
+ {"MessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"AccountSid", "ACe718619887aac3ee5b21edafbvsdf6h7fgb"},
+ {"From", "+10123456789"},
+ {"ApiVersion", "2010-04-01"}
+ };
+ private static readonly Dictionary HappyPathParameters = new()
+ {
+ {"ToCountry", "US"},
+ {"ToState", "OH"},
+ {"SmsMessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"NumMedia", "0"},
+ {"ToCity", "UTICA"},
+ {"FromZip", "20705"},
+ {"SmsSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"FromState", "DC"},
+ {"SmsStatus", "received"},
+ {"FromCity", "BELTSVILLE"},
+ {"Body", "Ahoy!"},
+ {"FromCountry", "US"},
+ {"To", "+10123456789"},
+ {"ToZip", "43037"},
+ {"NumSegments", "1"},
+ {"ReferralNumMedia", "0"},
+ {"MessageSid", "SMcea2a3bd6f50296f8fab60f377db03eb"},
+ {"AccountSid", "ACe718619887aac3ee5b21edafbvsdf6h7fgb"},
+ {"From", "+10123456789"},
+ {"ApiVersion", "2010-04-01"}
+ };
+
+
+ [Benchmark]
+ public void OriginalUnhappyPath()
+ {
+ var requestValidator = new RequestValidatorOriginal(Secret);
+ requestValidator.Validate(UnhappyPathUrl, UnhappyPathParameters, UnhappyPathSignature);
+ }
+
+ [Benchmark]
+ public void CurrentUnhappyPath()
+ {
+ var requestValidator = new RequestValidator(Secret);
+ requestValidator.Validate(UnhappyPathUrl, UnhappyPathParameters, UnhappyPathSignature);
+ }
+
+ [Benchmark]
+ public void OriginalHappyPath()
+ {
+ var requestValidator = new RequestValidatorOriginal(Secret);
+ requestValidator.Validate(HappyPathUrl, HappyPathParameters, HappyPathSignature);
+ }
+
+ [Benchmark]
+ public void CurrentHappyPath()
+ {
+ var requestValidator = new RequestValidator(Secret);
+ requestValidator.Validate(HappyPathUrl, HappyPathParameters, HappyPathSignature);
+ }
+}
\ No newline at end of file
diff --git a/test/Twilio.Benchmark/RequestValidatorOriginal.cs b/test/Twilio.Benchmark/RequestValidatorOriginal.cs
new file mode 100644
index 000000000..12b1a9947
--- /dev/null
+++ b/test/Twilio.Benchmark/RequestValidatorOriginal.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Twilio.Security
+{
+ ///
+ /// Twilio request validator
+ ///
+ public class RequestValidatorOriginal
+ {
+ private readonly HMACSHA1 _hmac;
+ private readonly SHA256 _sha;
+
+ ///
+ /// Create a new RequestValidator
+ ///
+ /// Signing secret
+ public RequestValidatorOriginal(string secret)
+ {
+ _hmac = new HMACSHA1(Encoding.UTF8.GetBytes(secret));
+ _sha = SHA256.Create();
+ }
+
+ ///
+ /// Validate against a request
+ ///
+ /// Request URL
+ /// Request parameters
+ /// Expected result
+ /// true if the signature matches the result; false otherwise
+ public bool Validate(string url, NameValueCollection parameters, string expected)
+ {
+ return Validate(url, ToDictionary(parameters), expected);
+ }
+
+ ///
+ /// Validate against a request
+ ///
+ /// Request URL
+ /// Request parameters
+ /// Expected result
+ /// true if the signature matches the result; false otherwise
+ public bool Validate(string url, IDictionary parameters, string expected)
+ {
+ // check signature of url with and without port, since sig generation on back end is inconsistent
+ var signatureWithoutPort = GetValidationSignature(RemovePort(url), parameters);
+ var signatureWithPort = GetValidationSignature(AddPort(url), parameters);
+ // If either url produces a valid signature, we accept the request as valid
+ return SecureCompare(signatureWithoutPort, expected) || SecureCompare(signatureWithPort, expected);
+ }
+
+ public bool Validate(string url, string body, string expected)
+ {
+ var paramString = new UriBuilder(url).Query.TrimStart('?');
+ var bodyHash = "";
+ foreach (var param in paramString.Split('&'))
+ {
+ var split = param.Split('=');
+ if (split[0] == "bodySHA256")
+ {
+ bodyHash = Uri.UnescapeDataString(split[1]);
+ }
+ }
+
+ return Validate(url, new Dictionary(), expected) && ValidateBody(body, bodyHash);
+ }
+
+ public bool ValidateBody(string rawBody, string expected)
+ {
+ var signature = _sha.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
+ return SecureCompare(BitConverter.ToString(signature).Replace("-", "").ToLower(), expected);
+ }
+
+ private static IDictionary ToDictionary(NameValueCollection col)
+ {
+ var dict = new Dictionary();
+ foreach (var k in col.AllKeys)
+ {
+ dict.Add(k, col[k]);
+ }
+ return dict;
+ }
+
+ private string GetValidationSignature(string url, IDictionary parameters)
+ {
+ var b = new StringBuilder(url);
+ if (parameters != null)
+ {
+ var sortedKeys = new List(parameters.Keys);
+ sortedKeys.Sort(StringComparer.Ordinal);
+
+ foreach (var key in sortedKeys)
+ {
+ b.Append(key).Append(parameters[key] ?? "");
+ }
+ }
+
+ var hash = _hmac.ComputeHash(Encoding.UTF8.GetBytes(b.ToString()));
+ return Convert.ToBase64String(hash);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ private static bool SecureCompare(string a, string b)
+ {
+ if (a == null || b == null)
+ {
+ return false;
+ }
+
+ var n = a.Length;
+ if (n != b.Length)
+ {
+ return false;
+ }
+
+ var mismatch = 0;
+ for (var i = 0; i < n; i++)
+ {
+ mismatch |= a[i] ^ b[i];
+ }
+
+ return mismatch == 0;
+ }
+
+ private string RemovePort(string url)
+ {
+ return SetPort(url, -1);
+ }
+
+ private string AddPort(string url)
+ {
+ var uri = new UriBuilder(url);
+ return SetPort(url, uri.Port);
+ }
+
+ private string SetPort(string url, int port)
+ {
+ var uri = new UriBuilder(url);
+ uri.Host = PreserveCase(url, uri.Host);
+ if (port == -1)
+ {
+ uri.Port = port;
+ }
+ else if ((port != 443) && (port != 80))
+ {
+ uri.Port = port;
+ }
+ else
+ {
+ uri.Port = uri.Scheme == "https" ? 443 : 80;
+ }
+ var scheme = PreserveCase(url, uri.Scheme);
+ return uri.Uri.OriginalString.Replace(uri.Scheme, scheme);
+ }
+
+ private string PreserveCase(string url, string replacementString)
+ {
+ var startIndex = url.IndexOf(replacementString, StringComparison.OrdinalIgnoreCase);
+ return url.Substring(startIndex, replacementString.Length);
+ }
+ }
+}
diff --git a/test/Twilio.Benchmark/Twilio.Benchmark.csproj b/test/Twilio.Benchmark/Twilio.Benchmark.csproj
new file mode 100644
index 000000000..81b91b034
--- /dev/null
+++ b/test/Twilio.Benchmark/Twilio.Benchmark.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ disable
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Twilio.Test/Security/RequestValidatorTest.cs b/test/Twilio.Test/Security/RequestValidatorTest.cs
index 22dd894f3..0d3b8646d 100644
--- a/test/Twilio.Test/Security/RequestValidatorTest.cs
+++ b/test/Twilio.Test/Security/RequestValidatorTest.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
+using System.Diagnostics;
+using System.Threading;
using NUnit.Framework;
using Twilio.Security;
@@ -121,7 +123,7 @@ public void TestValidateCollection()
[Test]
public void TestValidateBody()
{
- Assert.IsTrue(_validator.ValidateBody(Body, BodyHash), "Request body validation failed");
+ Assert.IsTrue(RequestValidator.ValidateBody(Body, BodyHash), "Request body validation failed");
}
[Test]
@@ -183,6 +185,31 @@ public void TestValidateAddsCustomPortHttp()
{
const string url = "http://mycompany.com:1234/myapp.php?foo=1&bar=2";
Assert.IsTrue(_validator.Validate(url, _parameters, "Zmvh+3yNM1Phv2jhDCwEM3q5ebU="), "Request does not match provided signature");
+ }
+
+ [Test]
+ public void TestIsThreadSafe()
+ {
+ var validator = new RequestValidator("secret");
+ var thread1 = new Thread(Validate);
+ var thread2 = new Thread(Validate);
+
+ Assert.DoesNotThrow(() =>
+ {
+ thread1.Start(validator);
+ thread2.Start(validator);
+ thread1.Join();
+ thread2.Join();
+ });
+ }
+
+ private static void Validate(object obj)
+ {
+ var sw = Stopwatch.StartNew();
+ while (sw.ElapsedMilliseconds < 5000)
+ {
+ ((RequestValidator)obj).Validate("https://foo.com", "123", "foo");
+ }
}
}
}
diff --git a/test/Twilio.Test/Twilio.Test.csproj b/test/Twilio.Test/Twilio.Test.csproj
index 6189e62c0..c807826a0 100644
--- a/test/Twilio.Test/Twilio.Test.csproj
+++ b/test/Twilio.Test/Twilio.Test.csproj
@@ -1,4 +1,4 @@
-
+
Exe
Twilio.Tests
@@ -8,7 +8,7 @@
false
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -30,4 +30,4 @@
-
+
\ No newline at end of file