Skip to content

Commit

Permalink
Add support for Sinch dashboard credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
zcausev committed Nov 13, 2024
1 parent 56bc61d commit eac4805
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 106 deletions.
69 changes: 46 additions & 23 deletions src/Sinch.ServerSdk/ApiFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,6 +9,8 @@
using Sinch.ServerSdk.Verification;
using Sinch.ServerSdk.Verification.Fluent;
using Sinch.WebApiClient;
using System;
using System.Text;

namespace Sinch.ServerSdk
{
Expand Down Expand Up @@ -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<IActionFilter> _signingFilterFactory;

/// <summary>
/// </summary>
/// <param name="key"></param>
Expand All @@ -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<ISmsApiEndpoints>(_url));
}


}

public ICalloutApi CreateCalloutApi()
{
return new CalloutApi(CreateApiClient<ICalloutApiEndpoints>(String.Format(_url, "calling")), new CallbackResponseFactory(_locale));

}


public IConferenceApi CreateConferenceApi()
{
Expand All @@ -137,12 +163,9 @@ private T CreateApiClient<T>() where T : class
{
return CreateApiClient<T>(_url);
}
private T CreateApiClient<T>(string url) where T : class
{
//var handler = new HttpClientHandler();
//handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
return new WebApiClientFactory().CreateClient<T>(url, new ApplicationSigningFilter(_key, _secret),

private T CreateApiClient<T>(string url) where T : class =>
new WebApiClientFactory().CreateClient<T>(url, _signingFilterFactory(),
new RestReplyFilter());
}
}
}
35 changes: 35 additions & 0 deletions src/Sinch.ServerSdk/ApiFilters/AccessCredentialsSigningFilter.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
28 changes: 2 additions & 26 deletions src/Sinch.ServerSdk/ApiFilters/ApplicationSigningFilter.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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));

Expand All @@ -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<ApiError>(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<string> BuildStringToSign(HttpRequestMessage request)
{
var sb = new StringBuilder();
Expand Down
34 changes: 34 additions & 0 deletions src/Sinch.ServerSdk/ApiFilters/SinchSigningFilterBase.cs
Original file line number Diff line number Diff line change
@@ -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<ApiError>(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);
}
}
}
}
53 changes: 53 additions & 0 deletions src/Sinch.ServerSdk/SinchAccessCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;

namespace Sinch.ServerSdk
{
/// <summary>
/// Sinch project credentials
/// </summary>
public class SinchAccessCredentials
{
/// <summary>
/// Project access key ID
/// </summary>
public readonly string AccessKeyId;

/// <summary>
/// Project key secret
/// </summary>
public readonly string KeySecret;

/// <summary>
/// Application identifier to be associated with the requests
/// </summary>
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);
}
}
Loading

0 comments on commit eac4805

Please sign in to comment.