Skip to content
Draft
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
30 changes: 28 additions & 2 deletions src/Indice.Services/SmsServiceApifon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ ILogger<SmsServiceApifon> logger
if (string.IsNullOrWhiteSpace(Settings.ApiKey)) {
throw new ArgumentException($"SMS settings {nameof(SmsServiceApifonSettings.ApiKey)} is empty.");
}
if (!string.IsNullOrWhiteSpace(Settings.WebhookUrl) && string.IsNullOrWhiteSpace(Settings.WebhookSecret)) {
throw new ArgumentException($"{nameof(SmsServiceApifonSettings.WebhookSecret)} cannot be empty if a {nameof(SmsServiceApifonSettings.WebhookUrl)} is provided.");
}
}

/// <summary>The Apifon base URL address.</summary>
Expand Down Expand Up @@ -60,13 +63,16 @@ public async Task<SendReceipt> SendAsync(string destination, string subject, str
throw new ArgumentException("Invalid recipients. Recipients should be valid phone numbers", nameof(recipient));
}
return phone.ToString("D");
})
.ToArray();
}).ToArray();
if (recipients.Any(phoneNumber => phoneNumber.Any(numberChar => !char.IsNumber(numberChar)))) {
throw new ArgumentException("Invalid recipients. Recipients cannot contain letters.", nameof(destination));
}
// https://docs.apifon.com/apireference.html#sms-request
var payload = ApifonRequest.CreateSms(sender?.Id ?? Settings.Sender ?? Settings.SenderName!, recipients, body!, Settings.EnableUrlShortener);
if (!string.IsNullOrWhiteSpace(Settings.WebhookUrl)) {
payload.WithCallbackUrl(Settings.WebhookUrl, Settings.WebhookSecret);
}

var signature = payload.Sign(Settings.ApiKey!, HttpMethod.Post.ToString(), SERVICE_ENDPOINT);
var request = new HttpRequestMessage {
Content = new StringContent(payload.ToJson(), Encoding.UTF8, "application/json"),
Expand Down Expand Up @@ -130,6 +136,10 @@ public class SmsServiceApifonSettings : SmsServiceSettings
public string Token { get; set; } = null!;
/// <summary>If enabled all urls in the message will be replaced with shortened urls</summary>
public bool EnableUrlShortener { get; set; } = false;
/// <summary>The base URL to be used for callback events.</summary>
public string WebhookUrl { get; set; } = string.Empty;
/// <summary>The secret to be used for hashing the request.</summary>
public string WebhookSecret { get; set; } = string.Empty;
}

internal class ApifonResponse
Expand Down Expand Up @@ -212,6 +222,8 @@ internal static Dictionary<string, ApifonListParameter> ExtractParametersAndRepl

[JsonPropertyName("message")]
public ApifonMessage Message { get; set; } = new();
[JsonPropertyName("reference_id")]
public string? ReferenceId { get; set; }
[JsonPropertyName("subscribers")]
public List<Subscriber> Subscribers { get; set; } = [];
/// <summary>SMS validity period. Min 30 - Max 4320 (default).</summary>
Expand Down Expand Up @@ -293,6 +305,7 @@ public static string ToJson<TRequest>(this TRequest request, JsonSerializerOptio
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});

/// <summary>Signs an Apifon request.</summary>
public static string Sign<TRequest>(this TRequest request, string secretKey, string method, string uri) where TRequest : ApifonRequest {
var toSign = method + "\n"
+ uri + "\n"
Expand All @@ -302,4 +315,17 @@ public static string Sign<TRequest>(this TRequest request, string secretKey, str
using var hmacSha256 = new HMACSHA256(encoding.GetBytes(secretKey));
return Convert.ToBase64String(hmacSha256.ComputeHash(encoding.GetBytes(toSign)));
}

/// <summary>Generates a callback URL with a reference identifier for an Apifon request.</summary>
public static TRequest WithCallbackUrl<TRequest>(this TRequest request, string baseUrl, string secretKey) where TRequest : ApifonRequest {
var timestamp = DateTimeOffset.UtcNow;
var referenceId = Guid.NewGuid().ToString("N");
var payload = $"{referenceId}:{timestamp:O}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var hex = Convert.ToHexString(hash);
request.ReferenceId = referenceId;
request.CallbackUrl = $"{baseUrl}?hmac={hex}&ref={referenceId}&ts={Uri.EscapeDataString(timestamp.ToString("O"))}";
return request;
}
}
10 changes: 6 additions & 4 deletions test/Indice.Services.Tests/SmsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public async Task TestApifonSms(string apiKey, string token, string phoneNumber,
["Sms:Sender"] = sender,
["Sms:SenderName"] = senderName,
["Sms:TestMode"] = true.ToString(),
["Sms:EnableUrlShortener"] = true.ToString()
["Sms:EnableUrlShortener"] = true.ToString(),
["Sms:WebhookUrl"] = string.Empty,
["Sms:WebhookSecret"] = string.Empty
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
Expand All @@ -56,15 +58,15 @@ public async Task TestApifonSms(string apiKey, string token, string phoneNumber,


var serviceProvider = collection.BuildServiceProvider();
var excepion = default(Exception);
var exception = default(Exception);

try {
var service = serviceProvider.GetRequiredService<ISmsService>();
await service.SendAsync(phoneNumber, subject, body);
} catch (Exception smsServiceException) {
excepion = smsServiceException;
exception = smsServiceException;
}
Assert.Null(excepion);
Assert.Null(exception);
}

[Theory(Skip = "Sensitive Data")]
Expand Down
Loading