Skip to content

Commit

Permalink
Merge pull request #105 from twilio-labs/releases/v7
Browse files Browse the repository at this point in the history
Merge v7 changes into main
  • Loading branch information
Swimburger authored Nov 17, 2022
2 parents fabd42b + ecf078d commit 9dc8ef0
Show file tree
Hide file tree
Showing 35 changed files with 424 additions and 473 deletions.
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,46 @@ app.MapPost("/sms", () => ...)
.AddEndpointFilter<ValidateTwilioRequestFilter>();
```

##### ASP.NET Core Middleware
When you can't use the `[ValidateRequest]` filter or `ValidateTwilioRequestFilter`, you can use the `ValidateTwilioRequestMiddleware` instead.
You can add add the `ValidateTwilioRequestFilter` like this:

```csharp
app.UseTwilioRequestValidation();
// or the equivalent: app.UseMiddleware<ValidateTwilioRequestMiddleware>();
```

This middleware will perform the validation for all requests.
If you don't want to apply the validation to all requests, you can use `app.UseWhen()` to run the middleware conditionally.

Here's an example of how to validate requests that start with path _/twilio-media_, as to protect media files that only the Twilio Proxy should be able to access:

```csharp
using System.Net;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Twilio.AspNet.Core;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTwilioRequestValidation();

var app = builder.Build();

app.UseWhen(
context => context.Request.Path.StartsWithSegments("/twilio-media", StringComparison.OrdinalIgnoreCase),
app => app.UseTwilioRequestValidation()
);

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "TwilioMedia")),
RequestPath = "/twilio-media"
});

app.Run();
```

#### Validate requests in ASP.NET MVC on .NET Framework

In your _Web.config_ you can configure request validation like shown below:
Expand Down Expand Up @@ -441,7 +481,7 @@ public class SmsController : TwilioController
}
```

#### Validate requests outside of MVC
#### Validate requests using the RequestValidationHelper

The `[ValidateRequest]` attribute only works for MVC. If you need to validate requests outside of MVC, you can use the `RequestValidationHelper` class provided by `Twilio.AspNet`.
Alternatively, the `RequestValidator` class from the [Twilio SDK](https://github.com/twilio/twilio-csharp) can also help you with this.
Expand Down Expand Up @@ -487,8 +527,7 @@ bool IsValidTwilioRequest(HttpContext httpContext)
urlOverride = $"{options.BaseUrlOverride.TrimEnd('/')}{request.Path}{request.QueryString}";
}

var validator = new RequestValidationHelper();
return validator.IsValidRequest(httpContext, options.AuthToken, urlOverride, options.AllowLocal ?? true);
return RequestValidationHelper.IsValidRequest(httpContext, options.AuthToken, urlOverride, options.AllowLocal ?? true);
}
```

Expand Down
20 changes: 17 additions & 3 deletions src/Twilio.AspNet.Common/SmsRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ namespace Twilio.AspNet.Common
/// <summary>
/// This class can be used as the parameter on your SMS action. Incoming parameters will be bound here.
/// </summary>
/// <remarks>http://www.twilio.com/docs/api/twiml/sms/twilio_request</remarks>
/// <remarks>https://www.twilio.com/docs/messaging/guides/webhook-request</remarks>
public class SmsRequest : TwilioRequest
{
/// <summary>
/// A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API
/// A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API.
/// </summary>
public string MessageSid { get; set; }

/// <summary>
/// Same value as MessageSid. Deprecated and included for backward compatibility.
/// </summary>
public string SmsSid { get; set; }

Expand All @@ -30,6 +35,15 @@ public class SmsRequest : TwilioRequest
/// A unique identifier of the messaging service
/// </summary>
public string MessagingServiceSid { get; set; }


/// <summary>
/// The number of media items associated with your message
/// </summary>
public int NumMedia { get; set; }

/// <summary>
/// The number of media items associated with a "Click to WhatsApp" advertisement.
/// </summary>
public int ReferralNumMedia { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class SmsStatusCallbackRequest: SmsRequest
/// status is failed or undelivered, the ErrorCode can give you more information
/// about the failure. If the message was delivered successfully, no ErrorCode
/// will be present. Find the possible values here:
/// https://www.twilio.com/docs/sms/api/message#delivery-related-errors
/// https://www.twilio.com/docs/sms/api/message-resource#delivery-related-errors
/// </summary>
public string ErrorCode { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/Twilio.AspNet.Common/StatusCallbackRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// This class can be used as the parameter on your StatusCallback action. Incoming parameters will be bound here.
/// </summary>
/// <remarks>http://www.twilio.com/docs/api/twiml/twilio_request#asynchronous</remarks>
/// <remarks>https://www.twilio.com/docs/voice/twiml#ending-the-call-callback-requests</remarks>
public class StatusCallbackRequest : VoiceRequest
{
/// <summary>
Expand Down
1 change: 0 additions & 1 deletion src/Twilio.AspNet.Common/TwilioRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public abstract class TwilioRequest
/// </remarks>
public string To { get; set; }


/// <summary>
/// The city of the caller
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/Twilio.AspNet.Common/VoiceRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// This class can be used as the parameter on your voice action. Incoming parameters will be bound here.
/// </summary>
/// <remarks>http://www.twilio.com/docs/api/twiml/twilio_request</remarks>
/// <remarks>https://www.twilio.com/docs/usage/webhooks/voice-webhooks</remarks>
public class VoiceRequest : TwilioRequest
{
/// <summary>
Expand Down Expand Up @@ -35,7 +35,15 @@ public class VoiceRequest : TwilioRequest
/// This parameter is set when the IncomingPhoneNumber that received the call has had its VoiceCallerIdLookup value set to true.
/// </summary>
public string CallerName { get; set; }


/// <summary>
/// A unique identifier for the call that created this leg. This parameter is not passed if this is the first leg of a call.
/// </summary>
public string ParentCallSid { get; set; }

/// <summary>A token string needed to invoke a forwarded call.</summary>
public string CallToken { get; set; }

#region Gather & Record Parameters

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Twilio.AspNet.Core.MinimalApi;
using Twilio.TwiML;
using Xunit;

namespace Twilio.AspNet.Core.UnitTests;

// ReSharper disable once InconsistentNaming
public class MinimalApiTwiMLResultTests
{
[Fact]
Expand All @@ -32,6 +30,6 @@ private static async Task ValidateTwimlResultWritesToResponseBody(TwiML.TwiML tw
Assert.Equal(twiMlResponse.ToString(), responseBody);
}

private static VoiceResponse GetVoiceResponse() => new VoiceResponse().Say("Hello World");
private static MessagingResponse GetMessagingResponse() => new MessagingResponse().Message("Hello World");
private static VoiceResponse GetVoiceResponse() => new VoiceResponse().Say("Ahoy!");
private static MessagingResponse GetMessagingResponse() => new MessagingResponse().Message("Ahoy!");
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ public class ContextMocks
public Moq.Mock<HttpContext> HttpContext { get; set; }
public Moq.Mock<HttpRequest> Request { get; set; }

public ContextMocks(bool isLocal, FormCollection? form = null, bool isProxied = false) : this("", isLocal, form, isProxied)
public ContextMocks(bool isLocal, FormCollection form = null, bool isProxied = false) : this("", isLocal, form, isProxied)
{
}

public ContextMocks(string urlOverride, bool isLocal, FormCollection? form = null, bool isProxied = false)
public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null, bool isProxied = false)
{
var headers = new HeaderDictionary();
headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form));
Expand Down Expand Up @@ -55,7 +55,7 @@ public ContextMocks(string urlOverride, bool isLocal, FormCollection? form = nul
public static string fakeUrl = "https://api.example.com/webhook";
public static string fakeAuthToken = "thisisafakeauthtoken";

private string CalculateSignature(string urlOverride, FormCollection? form)
private string CalculateSignature(string urlOverride, FormCollection form)
{
var value = new StringBuilder();
value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
Expand Down
13 changes: 7 additions & 6 deletions src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,10 @@ public void AddTwilioClient_With_ApiKeyOptions_Should_Match_Properties()
Assert.Equal(ValidTwilioOptions.Client.AccountSid, client.AccountSid);
Assert.Equal(ValidTwilioOptions.Client.LogLevel, client.LogLevel);
Assert.Equal(ValidTwilioOptions.Client.ApiKeySid,
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(client));
Assert.Equal(ValidTwilioOptions.Client.ApiKeySecret,
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(client));
}

Expand All @@ -236,10 +236,10 @@ public void AddTwilioClient_With_AuthTokenOptions_Should_Match_Properties()
Assert.Equal(ValidTwilioOptions.Client.AccountSid, client.AccountSid);
Assert.Equal(ValidTwilioOptions.Client.LogLevel, client.LogLevel);
Assert.Equal(ValidTwilioOptions.Client.AccountSid,
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)
typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(client));
Assert.Equal(ValidTwilioOptions.Client.AuthToken,
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)
typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(client));
}
[Fact]
Expand All @@ -257,7 +257,7 @@ public void AddTwilioClient_Without_HttpClientProvider_Should_Named_HttpClient()
var twilioRestClient = scope.ServiceProvider.GetService<TwilioRestClient>();

var actualHttpClient = (System.Net.Http.HttpClient) typeof(SystemNetHttpClient)
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(twilioRestClient.HttpClient);

Assert.NotNull(actualHttpClient);
Expand All @@ -271,14 +271,15 @@ public void AddTwilioClient_With_HttpClientProvider_Should_Use_HttpClient()
serviceCollection.AddSingleton(BuildValidConfiguration());

using var httpClient = new System.Net.Http.HttpClient();
// ReSharper disable once AccessToDisposedClosure
serviceCollection.AddTwilioClient(_ => httpClient);

var serviceProvider = serviceCollection.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();

var twilioRestClient = scope.ServiceProvider.GetService<TwilioRestClient>();
var httpClientFromTwilioClient = (System.Net.Http.HttpClient) typeof(SystemNetHttpClient)
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)
.GetField("_httpClient", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(twilioRestClient.HttpClient);

Assert.Equal(httpClient, httpClientFromTwilioClient);
Expand Down
23 changes: 19 additions & 4 deletions src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
Expand All @@ -17,8 +18,8 @@ public class TwilioControllerExtensionTests
[Fact]
public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody()
{
var twiml = new VoiceResponse().Say("Hello World");
var result = TwilioControllerExtensions.TwiML(Mock.Of<ControllerBase>(), twiml);
var twiml = new VoiceResponse().Say("Ahoy!");
var result = Mock.Of<ControllerBase>().TwiML(twiml);
var actionContext = CreateActionContext();
await result.ExecuteResultAsync(actionContext);

Expand All @@ -27,12 +28,26 @@ public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody()
var responseBody = await reader.ReadToEndAsync();
Assert.Equal(twiml.ToString(), responseBody);
}

[Fact]
public async Task TwimlResult_Should_Write_VoiceResponse_To_ResponseBody_Unformatted()
{
var twiml = new VoiceResponse().Say("Ahoy!");
var result = Mock.Of<ControllerBase>().TwiML(twiml, SaveOptions.DisableFormatting);
var actionContext = CreateActionContext();
await result.ExecuteResultAsync(actionContext);

actionContext.HttpContext.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(actionContext.HttpContext.Response.Body);
var responseBody = await reader.ReadToEndAsync();
Assert.Equal(twiml.ToString(SaveOptions.DisableFormatting), responseBody);
}

[Fact]
public async Task TwimlResult_Should_Write_MessagingResponse_To_ResponseBody()
{
var twiml = new MessagingResponse().Message("Hello World");
var result = TwilioControllerExtensions.TwiML(Mock.Of<ControllerBase>(), twiml);
var twiml = new MessagingResponse().Message("Ahoy!");
var result = Mock.Of<ControllerBase>().TwiML(twiml);
var actionContext = CreateActionContext();
await result.ExecuteResultAsync(actionContext);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
Expand Down Expand Up @@ -128,13 +126,13 @@ public void ValidateRequestAttribute_Validates_Request_Successfully()
var actionExecutingContext = new ActionExecutingContext(
new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()),
new List<IFilterMetadata>(),
new Dictionary<string, object?>(),
new Dictionary<string, object>(),
new object()
);

attribute.OnActionExecuting(actionExecutingContext);

Assert.Equal(null, actionExecutingContext.Result);
Assert.Null(actionExecutingContext.Result);
}

[Fact]
Expand All @@ -156,13 +154,13 @@ public void ValidateRequestFilter_Validates_Request_Forbid()
var actionExecutingContext = new ActionExecutingContext(
new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()),
new List<IFilterMetadata>(),
new Dictionary<string, object?>(),
new Dictionary<string, object>(),
new object()
);

attribute.OnActionExecuting(actionExecutingContext);

var statusCodeResult = (HttpStatusCodeResult)actionExecutingContext.Result!;
var statusCodeResult = (StatusCodeResult)actionExecutingContext.Result!;
Assert.NotNull(statusCodeResult);
Assert.Equal((int)HttpStatusCode.Forbidden, statusCodeResult.StatusCode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public async Task ValidateRequestFilter_Validates_Request_Successfully()

var result = await filter.InvokeAsync(
new DefaultEndpointFilterInvocationContext(fakeContext),
_ => ValueTask.FromResult<object?>(Results.Ok())
_ => ValueTask.FromResult<object>(Results.Ok())
);

Assert.IsType<Ok>(result);
Expand Down Expand Up @@ -103,7 +103,7 @@ public async Task ValidateRequestFilter_Validates_Request_Forbid()

var result = await filter.InvokeAsync(
new DefaultEndpointFilterInvocationContext(fakeContext),
_ => ValueTask.FromResult<object?>(Results.Ok())
_ => ValueTask.FromResult<object>(Results.Ok())
);

var statusCodeResult = (StatusCodeHttpResult)result!;
Expand Down
Loading

0 comments on commit 9dc8ef0

Please sign in to comment.