Skip to content

Instantly share code, notes, and snippets.

@eldavido
Created July 13, 2018 16:58
Show Gist options
  • Save eldavido/69b1e5a809cc1198ce01a28d081c181c to your computer and use it in GitHub Desktop.
Save eldavido/69b1e5a809cc1198ce01a28d081c181c to your computer and use it in GitHub Desktop.
Twilio client from interval
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Twilio.Security;
namespace interval.Support.Twilio {
public class TwilioClient {
private readonly ILogger<TwilioClient> _log;
private readonly string _accountSid;
private readonly string _authToken;
private readonly Uri _baseUrl;
private const string TwilioSigHeaderKey = "X-Twilio-Signature";
private readonly RequestValidator _rv;
public TwilioClient(ILoggerFactory lf, string sid, string authToken, string baseUrl) {
_log = lf.CreateLogger<TwilioClient>();
_accountSid = sid;
_authToken = authToken;
_baseUrl = new Uri(baseUrl);
_rv = new RequestValidator(authToken);
}
public async Task<TwilioNumber?> AllocateNumberForSmsConnection() {
using (var tc = Client) {
// Find a new number
var searchRq = await tc.GetAsync($"AvailablePhoneNumbers/US/Local.json?SmsEnabled=true")
.ConfigureAwait(false);
var searchRsp = await searchRq.Content.ReadAsStringAsync();
// Parse the response
var twilioResp = JObject.Parse(searchRsp);
var numbers = twilioResp.GetValue("available_phone_numbers").ToArray();
if (numbers.Length < 1) {
throw new Exception("No numbers returned from Twilio");
}
var desiredNumber = ((JObject)numbers[0]).GetValue("phone_number").ToString();
_log.LogInformation($"Attempting to allocate {desiredNumber} from Twilio");
// Provision the new number
var provisionParams = new Dictionary<string, string> {
{ "PhoneNumber", desiredNumber },
{ "SmsUrl", new Uri(_baseUrl, "/twilio").ToString() },
};
var provisionRq = await tc.PostAsync($"IncomingPhoneNumbers/Local.json",
Dict2Form(provisionParams)).ConfigureAwait(false);
var provisionRsp = await provisionRq.Content.ReadAsStringAsync();
var rsp = JObject.Parse(provisionRsp);
var sid = (string) rsp.GetValue("sid");
var provisionedNumber = (string) rsp.GetValue("phone_number");
if (provisionedNumber != desiredNumber) {
_log.LogWarning($"Provisioned number {provisionedNumber} was not desired number {desiredNumber}");
}
_log.LogInformation($"Provisioned {provisionedNumber} from twilio. sc={provisionRq.StatusCode}, rsp={provisionRsp}");
return new TwilioNumber(sid, new NanpPstnEndpoint(provisionedNumber));
}
}
public async Task<bool> ReleaseNumber(TwilioNumber n) {
using (var tc = Client) {
_log.LogInformation($"De-provisioning twilio number. sid={n.Sid}, pstn={n.Endpoint}");
var rsp = await tc.DeleteAsync($"IncomingPhoneNumbers/{n.Sid}");
_log.LogInformation($"Response from twilio: {rsp.StatusCode}");
return rsp.StatusCode == HttpStatusCode.NoContent;
}
}
public async Task SendSmsAsync(SmsMessage msg) {
using (var tc = Client) {
var d = new Dictionary<string, string> {
{ "To", msg.Recipient.NormalizedNumber },
{ "From", msg.Sender.NormalizedNumber },
{ "Body", msg.Body } };
await tc.PostAsync($"Messages.json", Dict2Form(d)).ConfigureAwait(false);
}
}
public bool RequestHasValidSignature(HttpRequest rq, Dictionary<string, string> postParams) {
if (!rq.Headers.ContainsKey(TwilioSigHeaderKey)) {
return false;
}
var twilioSignature = rq.Headers[TwilioSigHeaderKey];
// NOTE(DA) Test gate: sid=test. Safeguard: this will never work in prod (no outgoing sends)
// so is likely to be caught quickly, in the unlikely event this is misdeployed to prod.
// NOTE(DA) (2) Hardcode https here so that the hook delivery always arrives to us as https.
// However, it may get rewritten by a reverse proxy, so we hardcode https here.
var path = string.Concat("https://", rq.Host.ToUriComponent(), rq.PathBase.ToUriComponent(),
rq.Path.ToUriComponent());
return (_accountSid == "test" && postParams.ContainsKey("bypass_twilio_check")) ||
_rv.Validate(path, postParams, twilioSignature);
}
private HttpClient Client {
get {
var tc = new HttpClient { BaseAddress = new Uri($"https://api.twilio.com/2010-04-01/Accounts/{_accountSid}/") };
tc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_accountSid}:{_authToken}")));
return tc;
}
}
private FormUrlEncodedContent Dict2Form(Dictionary<string, string> d) {
var kvps = d.Select(x => new KeyValuePair<string, string>(x.Key, x.Value));
return new FormUrlEncodedContent(kvps);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment