Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Sinch dashboard credentials #39

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
}
2 changes: 1 addition & 1 deletion src/Sinch.ServerSdk/Sinch.ServerSdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<AssemblyTitle>Sinch Server SDK</AssemblyTitle>
<Title>Sinch Server SDK</Title>
<Authors>Sinch AB</Authors>
<Version>2.3.0</Version>
<Version>2.4.0</Version>
<TargetFrameworks>netstandard2.0;net462; net45</TargetFrameworks>
<AssemblyName>Sinch.ServerSdk</AssemblyName>
<PackageId>Sinch.ServerSdk</PackageId>
Expand Down
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