From eac480545fc30760743db2feffaf5528c57fb14a Mon Sep 17 00:00:00 2001 From: Zoran Chaushev Date: Wed, 13 Nov 2024 17:39:34 +0100 Subject: [PATCH 1/2] Add support for Sinch dashboard credentials --- src/Sinch.ServerSdk/ApiFactory.cs | 69 +++++--- .../AccessCredentialsSigningFilter.cs | 35 ++++ .../ApiFilters/ApplicationSigningFilter.cs | 28 +--- .../ApiFilters/SinchSigningFilterBase.cs | 34 ++++ src/Sinch.ServerSdk/SinchAccessCredentials.cs | 53 ++++++ src/Sinch.ServerSdk/SinchFactory.cs | 151 +++++++++++------- 6 files changed, 264 insertions(+), 106 deletions(-) create mode 100644 src/Sinch.ServerSdk/ApiFilters/AccessCredentialsSigningFilter.cs create mode 100644 src/Sinch.ServerSdk/ApiFilters/SinchSigningFilterBase.cs create mode 100644 src/Sinch.ServerSdk/SinchAccessCredentials.cs diff --git a/src/Sinch.ServerSdk/ApiFactory.cs b/src/Sinch.ServerSdk/ApiFactory.cs index 4fc3b59..e2707a0 100644 --- a/src/Sinch.ServerSdk/ApiFactory.cs +++ b/src/Sinch.ServerSdk/ApiFactory.cs @@ -1,6 +1,4 @@ -using System; -using System.Net.Http; -using Sinch.ServerSdk.ApiFilters; +using Sinch.ServerSdk.ApiFilters; using Sinch.ServerSdk.Callback; using Sinch.ServerSdk.Calling; using Sinch.ServerSdk.Calling.Fluent; @@ -11,6 +9,8 @@ using Sinch.ServerSdk.Verification; using Sinch.ServerSdk.Verification.Fluent; using Sinch.WebApiClient; +using System; +using System.Text; namespace Sinch.ServerSdk { @@ -48,10 +48,12 @@ public interface IApiFactory internal class ApiFactory : IApiFactory { - private readonly string _key; - private readonly byte[] _secret; private readonly string _url; private readonly Locale _locale; + private readonly ICallbackValidator _callbackValidator; + + private readonly Func _signingFilterFactory; + /// /// /// @@ -78,50 +80,74 @@ internal ApiFactory(string key, string secret, Locale locale, string url = "http throw new ArgumentException( "Replace the Sinch application key with the one copied from your Sinch developer dashboard."); - _key = key; - if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret), "Sinch application secret cannot be null."); _locale = locale; + + if (string.IsNullOrWhiteSpace(url)) + throw new ArgumentNullException(nameof(url), "Sinch API URL cannot be null."); + + if (!Uri.TryCreate(String.Format(url, "calling"), UriKind.Absolute, out _)) + throw new ArgumentException( + "Sinch API URL is in an invalid format. The default URL is https://api.sinch.com"); + + _url = url; + + byte[] secretKey = ParseSecretKey(secret); + _signingFilterFactory = + () => new ApplicationSigningFilter(key, secretKey); + + _callbackValidator = new CallbackValidator(key, secretKey); + } + + private byte[] ParseSecretKey(string secret) + { try { - _secret = Convert.FromBase64String(secret.Trim()); + return Convert.FromBase64String(secret.Trim()); } catch (FormatException) { throw new ArgumentException( "Sinch application secret is in an invalid format. Confirm the secret is correctly copied from your Sinch developer dashboard."); } + } + + internal ApiFactory(SinchAccessCredentials credentials, Locale locale, string url = "https://{0}-use1-api.sinch.com") + { + if (credentials == null) + throw new ArgumentNullException(nameof(credentials)); + + _locale = locale ?? throw new ArgumentNullException(nameof(locale)); if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url), "Sinch API URL cannot be null."); - if (!Uri.TryCreate(String.Format(url, "calling"), UriKind.Absolute, out _)) - throw new ArgumentException( - "Sinch API URL is in an invalid format. The default URL is https://api.sinch.com"); - _url = url; + + _signingFilterFactory = () => new AccessCredentialsSigningFilter(credentials); + + _callbackValidator = new CallbackValidator( + credentials.AccessKeyId, Encoding.ASCII.GetBytes(credentials.KeySecret)); } public ICallbackValidator CreateCallbackValidator() { - return new CallbackValidator(_key, _secret); + return _callbackValidator; } public ISmsApi CreateSmsApi() { return new SmsApi(CreateApiClient(_url)); - } - - + } + public ICalloutApi CreateCalloutApi() { return new CalloutApi(CreateApiClient(String.Format(_url, "calling")), new CallbackResponseFactory(_locale)); } - public IConferenceApi CreateConferenceApi() { @@ -137,12 +163,9 @@ private T CreateApiClient() where T : class { return CreateApiClient(_url); } - private T CreateApiClient(string url) where T : class - { - //var handler = new HttpClientHandler(); - //handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12; - return new WebApiClientFactory().CreateClient(url, new ApplicationSigningFilter(_key, _secret), + + private T CreateApiClient(string url) where T : class => + new WebApiClientFactory().CreateClient(url, _signingFilterFactory(), new RestReplyFilter()); - } } } \ No newline at end of file diff --git a/src/Sinch.ServerSdk/ApiFilters/AccessCredentialsSigningFilter.cs b/src/Sinch.ServerSdk/ApiFilters/AccessCredentialsSigningFilter.cs new file mode 100644 index 0000000..febf87b --- /dev/null +++ b/src/Sinch.ServerSdk/ApiFilters/AccessCredentialsSigningFilter.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; + +namespace Sinch.ServerSdk.ApiFilters +{ + internal class AccessCredentialsSigningFilter : SinchSigningFilterBase + { + private readonly AuthenticationHeaderValue _authHeader; + private readonly string _applicationKey; + + public AccessCredentialsSigningFilter(SinchAccessCredentials credentials) + { + if (credentials == null) + throw new ArgumentNullException(nameof(credentials)); + + _applicationKey = credentials.ApplicationKey; + + _authHeader = new AuthenticationHeaderValue("Basic", + Convert.ToBase64String(Encoding.ASCII.GetBytes($"{credentials.AccessKeyId}:{credentials.KeySecret}"))); + } + + public override Task OnActionExecuting(HttpRequestMessage requestMessage) + { + requestMessage.Headers.Authorization = _authHeader; + requestMessage.Headers.Add("X-Sinch-AuthType", "zap"); + requestMessage.Headers.Add("X-Sinch-ApplicationKey", _applicationKey); + + // net45 does not have Task.CompletedTask + return Task.FromResult(false); + } + } +} \ No newline at end of file diff --git a/src/Sinch.ServerSdk/ApiFilters/ApplicationSigningFilter.cs b/src/Sinch.ServerSdk/ApiFilters/ApplicationSigningFilter.cs index 8cd2ec9..5ac9c69 100644 --- a/src/Sinch.ServerSdk/ApiFilters/ApplicationSigningFilter.cs +++ b/src/Sinch.ServerSdk/ApiFilters/ApplicationSigningFilter.cs @@ -1,17 +1,14 @@ using System; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; -using Newtonsoft.Json; -using Sinch.WebApiClient; namespace Sinch.ServerSdk.ApiFilters { - public class ApplicationSigningFilter : IActionFilter + internal class ApplicationSigningFilter : SinchSigningFilterBase { readonly string _key; readonly byte[] _secret; @@ -22,7 +19,7 @@ public ApplicationSigningFilter(string key, byte[] secret) _secret = secret; } - public async Task OnActionExecuting(HttpRequestMessage requestMessage) + public override async Task OnActionExecuting(HttpRequestMessage requestMessage) { requestMessage.Headers.Add("x-timestamp", DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)); @@ -35,27 +32,6 @@ public async Task OnActionExecuting(HttpRequestMessage requestMessage) } } - public async Task OnActionExecuted(HttpResponseMessage responseMessage) - { - if (responseMessage.StatusCode != HttpStatusCode.OK && - responseMessage.StatusCode != HttpStatusCode.NoContent) - { - var value = await responseMessage.Content.ReadAsStringAsync(); - ApiError error; - try - { - error = JsonConvert.DeserializeObject(value) ?? - new ApiError { ErrorCode = (int)responseMessage.StatusCode, Message = "Unable to deserialize exception (because it seems to be empty): " + value }; - } - catch (JsonSerializationException) - { - error = new ApiError { ErrorCode = (int)responseMessage.StatusCode, Message = "Unable to deserialize exception: " + value }; - } - - throw new ApiException(error); - } - } - static async Task BuildStringToSign(HttpRequestMessage request) { var sb = new StringBuilder(); diff --git a/src/Sinch.ServerSdk/ApiFilters/SinchSigningFilterBase.cs b/src/Sinch.ServerSdk/ApiFilters/SinchSigningFilterBase.cs new file mode 100644 index 0000000..592792d --- /dev/null +++ b/src/Sinch.ServerSdk/ApiFilters/SinchSigningFilterBase.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Sinch.WebApiClient; + +namespace Sinch.ServerSdk.ApiFilters +{ + internal abstract class SinchSigningFilterBase : IActionFilter + { + public abstract Task OnActionExecuting(HttpRequestMessage requestMessage); + + public async Task OnActionExecuted(HttpResponseMessage responseMessage) + { + if (responseMessage.StatusCode != HttpStatusCode.OK && + responseMessage.StatusCode != HttpStatusCode.NoContent) + { + var value = await responseMessage.Content.ReadAsStringAsync(); + ApiError error; + try + { + error = JsonConvert.DeserializeObject(value) ?? + new ApiError { ErrorCode = (int)responseMessage.StatusCode, Message = "Unable to deserialize exception (because it seems to be empty): " + value }; + } + catch (JsonSerializationException) + { + error = new ApiError { ErrorCode = (int)responseMessage.StatusCode, Message = "Unable to deserialize exception: " + value }; + } + + throw new ApiException(error); + } + } + } +} \ No newline at end of file diff --git a/src/Sinch.ServerSdk/SinchAccessCredentials.cs b/src/Sinch.ServerSdk/SinchAccessCredentials.cs new file mode 100644 index 0000000..c3e6e23 --- /dev/null +++ b/src/Sinch.ServerSdk/SinchAccessCredentials.cs @@ -0,0 +1,53 @@ +using System; + +namespace Sinch.ServerSdk +{ + /// + /// Sinch project credentials + /// + public class SinchAccessCredentials + { + /// + /// Project access key ID + /// + public readonly string AccessKeyId; + + /// + /// Project key secret + /// + public readonly string KeySecret; + + /// + /// Application identifier to be associated with the requests + /// + public readonly string ApplicationKey; + + public SinchAccessCredentials(string accessKeyId, string keySecret, string applicationKey) + { + if (accessKeyId == null) + throw new ArgumentNullException(nameof(accessKeyId)); + + if (string.Empty.Equals(accessKeyId)) + throw new ArgumentException($"{nameof(accessKeyId)} must be a non-empty string", nameof(accessKeyId)); + + if (keySecret == null) + throw new ArgumentNullException(nameof(keySecret)); + + if (string.Empty.Equals(keySecret)) + throw new ArgumentException($"{nameof(keySecret)} must be a non-empty string", nameof(keySecret)); + + if (applicationKey == null) + throw new ArgumentNullException(nameof(applicationKey)); + + if (string.Empty.Equals(applicationKey)) + throw new ArgumentException($"{nameof(applicationKey)} must be a non-empty string", nameof(applicationKey)); + + AccessKeyId = accessKeyId; + KeySecret = keySecret; + ApplicationKey = applicationKey; + } + + public static SinchAccessCredentials Create(string accessKeyId, string keySecret, string applicationKey) => + new SinchAccessCredentials(accessKeyId, keySecret, applicationKey); + } +} \ No newline at end of file diff --git a/src/Sinch.ServerSdk/SinchFactory.cs b/src/Sinch.ServerSdk/SinchFactory.cs index e10191d..5d3754d 100644 --- a/src/Sinch.ServerSdk/SinchFactory.cs +++ b/src/Sinch.ServerSdk/SinchFactory.cs @@ -1,58 +1,95 @@ -using Sinch.ServerSdk.Models; - -namespace Sinch.ServerSdk -{ - /// - /// Entry-point in to the SinchServerSdk. Used to create callback response factories and Api factories for issuing requests - /// to the Sinch Products - /// - public class SinchFactory - { - /// - /// Creates a factory for creating callback responses. - /// - /// The locale for the callback response factory. If null defaults to en-US - /// An instance of - public static ICallbackResponseFactory CreateCallbackResponseFactory(Locale locale) - { - return new CallbackResponseFactory(locale); - } - - /// - /// Creates a factory for creating a Sinch Application API factory. - /// - /// Your application key - /// Your application secret - /// - /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will - /// https://*-euc1.api.sinch.com/[version] - Europe - /// https://*-use1.api.sinch.com/[version] - United States - /// https://*-sae1.api.sinch.com/[version] - South America - /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 - /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 - /// An instance of - public static IApiFactory CreateApiFactory(string key, string secret, Locale locale, string url = "https://{0}-use1.api.sinch.com") - { - return new ApiFactory(key, secret, locale, url); - } - - /// - /// Creates a factory for creating a Sinch Application API factory. - /// - /// Your application key - /// Your application secret - /// The locale for the callback response factory. If null defaults to en-US - /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will - /// https://*-euc1.api.sinch.com/[version] - Europe - /// https://*-use1.api.sinch.com/[version] - United States - /// https://*-sae1.api.sinch.com/[version] - South America - /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 - /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 - /// - /// An instance of - public static IApiFactory CreateApiFactory(string key, string secret, string url = "https://{0}-use1.api.sinch.com") - { - return CreateApiFactory(key, secret, Locale.EnUs, url); - } - } +using Sinch.ServerSdk.Models; + +namespace Sinch.ServerSdk +{ + /// + /// Entry-point in to the SinchServerSdk. Used to create callback response factories and Api factories for issuing requests + /// to the Sinch Products + /// + public class SinchFactory + { + /// + /// Creates a factory for creating callback responses. + /// + /// The locale for the callback response factory. If null defaults to en-US + /// An instance of + public static ICallbackResponseFactory CreateCallbackResponseFactory(Locale locale) + { + return new CallbackResponseFactory(locale); + } + + /// + /// Creates a factory for creating a Sinch Application API factory. + /// + /// Your application key + /// Your application secret + /// Default locale + /// + /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will + /// https://*-euc1.api.sinch.com/[version] - Europe + /// https://*-use1.api.sinch.com/[version] - United States + /// https://*-sae1.api.sinch.com/[version] - South America + /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 + /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 + /// An instance of + public static IApiFactory CreateApiFactory(string key, string secret, Locale locale, string url = "https://{0}-use1.api.sinch.com") + { + return new ApiFactory(key, secret, locale, url); + } + + /// + /// Creates a factory for creating a Sinch Application API factory. + /// + /// Your Sinch project credentials + /// Default locale + /// + /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will + /// https://*-euc1.api.sinch.com/[version] - Europe + /// https://*-use1.api.sinch.com/[version] - United States + /// https://*-sae1.api.sinch.com/[version] - South America + /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 + /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 + /// An instance of + public static IApiFactory CreateApiFactory(SinchAccessCredentials credentials, Locale locale, + string url = "https://{0}-use1.api.sinch.com") + { + return new ApiFactory(credentials, locale, url); + } + + /// + /// Creates a factory for creating a Sinch Application API factory. + /// + /// Your application key + /// Your application secret + /// The locale for the callback response factory. If null defaults to en-US + /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will + /// https://*-euc1.api.sinch.com/[version] - Europe + /// https://*-use1.api.sinch.com/[version] - United States + /// https://*-sae1.api.sinch.com/[version] - South America + /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 + /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 + /// + /// An instance of + public static IApiFactory CreateApiFactory(string key, string secret, string url = "https://{0}-use1.api.sinch.com") + { + return CreateApiFactory(key, secret, Locale.EnUs, url); + } + + /// + /// Creates a factory for creating a Sinch Application API factory. + /// + /// Your Sinch project credentials + /// /// Its different enpints for each region, to initialise with one specific region pass in https://{0}-use1-api.sinch.com 0 will + /// https://*-euc1.api.sinch.com/[version] - Europe + /// https://*-use1.api.sinch.com/[version] - United States + /// https://*-sae1.api.sinch.com/[version] - South America + /// https://*-apse1.api.sinch.com/[version] - South East Asia 1 + /// https://*-apse2.api.sinch.com/[version] - South East Asia 2 + /// + /// An instance of + public static IApiFactory CreateApiFactory(SinchAccessCredentials credentials, string url = "https://{0}-use1.api.sinch.com") + { + return CreateApiFactory(credentials, Locale.EnUs, url); + } + } } \ No newline at end of file From c30ed51866d3ebd1f77ad827fc8ede6d4ab8a955 Mon Sep 17 00:00:00 2001 From: Zoran Chaushev Date: Wed, 20 Nov 2024 09:55:03 +0100 Subject: [PATCH 2/2] Update package version --- src/Sinch.ServerSdk/Sinch.ServerSdk.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sinch.ServerSdk/Sinch.ServerSdk.csproj b/src/Sinch.ServerSdk/Sinch.ServerSdk.csproj index 2a388d5..14f54c3 100644 --- a/src/Sinch.ServerSdk/Sinch.ServerSdk.csproj +++ b/src/Sinch.ServerSdk/Sinch.ServerSdk.csproj @@ -6,7 +6,7 @@ Sinch Server SDK Sinch Server SDK Sinch AB - 2.3.0 + 2.4.0 netstandard2.0;net462; net45 Sinch.ServerSdk Sinch.ServerSdk