8000 Merge v7 changes into main by Swimburger · Pull Request #105 · twilio-labs/twilio-aspnet · GitHub
[go: up one dir, main page]

Skip to content

Merge v7 changes into main #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and conta 8000 ct 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

Merged
merged 16 commits into from
Nov 17, 2022
Merged
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
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