diff --git a/README.md b/README.md index af39551..15d2d10 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,46 @@ app.MapPost("/sms", () => ...) .AddEndpointFilter(); ``` +##### 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(); +``` + +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: @@ -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. @@ -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); } ``` diff --git a/src/Twilio.AspNet.Common/SmsRequest.cs b/src/Twilio.AspNet.Common/SmsRequest.cs index a202f1a..b5a5c46 100644 --- a/src/Twilio.AspNet.Common/SmsRequest.cs +++ b/src/Twilio.AspNet.Common/SmsRequest.cs @@ -3,11 +3,16 @@ namespace Twilio.AspNet.Common /// /// This class can be used as the parameter on your SMS action. Incoming parameters will be bound here. /// - /// http://www.twilio.com/docs/api/twiml/sms/twilio_request + /// https://www.twilio.com/docs/messaging/guides/webhook-request public class SmsRequest : TwilioRequest { /// - /// 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. + /// + public string MessageSid { get; set; } + + /// + /// Same value as MessageSid. Deprecated and included for backward compatibility. /// public string SmsSid { get; set; } @@ -30,6 +35,15 @@ public class SmsRequest : TwilioRequest /// A unique identifier of the messaging service /// public string MessagingServiceSid { get; set; } - + + /// + /// The number of media items associated with your message + /// + public int NumMedia { get; set; } + + /// + /// The number of media items associated with a "Click to WhatsApp" advertisement. + /// + public int ReferralNumMedia { get; set; } } } diff --git a/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs b/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs index 82e1dae..2736dfc 100644 --- a/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs +++ b/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs @@ -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 /// public string ErrorCode { get; set; } diff --git a/src/Twilio.AspNet.Common/StatusCallbackRequest.cs b/src/Twilio.AspNet.Common/StatusCallbackRequest.cs index f2bdbc4..d42edb1 100644 --- a/src/Twilio.AspNet.Common/StatusCallbackRequest.cs +++ b/src/Twilio.AspNet.Common/StatusCallbackRequest.cs @@ -3,7 +3,7 @@ /// /// This class can be used as the parameter on your StatusCallback action. Incoming parameters will be bound here. /// - /// http://www.twilio.com/docs/api/twiml/twilio_request#asynchronous + /// https://www.twilio.com/docs/voice/twiml#ending-the-call-callback-requests public class StatusCallbackRequest : VoiceRequest { /// diff --git a/src/Twilio.AspNet.Common/TwilioRequest.cs b/src/Twilio.AspNet.Common/TwilioRequest.cs index 4968186..6e02eaf 100644 --- a/src/Twilio.AspNet.Common/TwilioRequest.cs +++ b/src/Twilio.AspNet.Common/TwilioRequest.cs @@ -26,7 +26,6 @@ public abstract class TwilioRequest /// public string To { get; set; } - /// /// The city of the caller /// diff --git a/src/Twilio.AspNet.Common/VoiceRequest.cs b/src/Twilio.AspNet.Common/VoiceRequest.cs index 9b81522..fa86399 100644 --- a/src/Twilio.AspNet.Common/VoiceRequest.cs +++ b/src/Twilio.AspNet.Common/VoiceRequest.cs @@ -3,7 +3,7 @@ /// /// This class can be used as the parameter on your voice action. Incoming parameters will be bound here. /// - /// http://www.twilio.com/docs/api/twiml/twilio_request + /// https://www.twilio.com/docs/usage/webhooks/voice-webhooks public class VoiceRequest : TwilioRequest { /// @@ -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. /// public string CallerName { get; set; } - + + /// + /// A unique identifier for the call that created this leg. This parameter is not passed if this is the first leg of a call. + /// + public string ParentCallSid { get; set; } + + /// A token string needed to invoke a forwarded call. + public string CallToken { get; set; } + #region Gather & Record Parameters /// diff --git a/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs b/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs index 99df76f..134b55c 100644 --- a/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs @@ -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] @@ -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!"); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs b/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs index 36752a3..3be4940 100644 --- a/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs @@ -15,11 +15,11 @@ public class ContextMocks public Moq.Mock HttpContext { get; set; } public Moq.Mock 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)); @@ -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); diff --git a/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj b/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj index f18683b..69f4276 100644 --- a/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj +++ b/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj @@ -1,8 +1,8 @@ net7.0 - enable false + disable diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs index 5b52e05..a2c3571 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs @@ -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)); } @@ -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] @@ -257,7 +257,7 @@ public void AddTwilioClient_Without_HttpClientProvider_Should_Named_HttpClient() var twilioRestClient = scope.ServiceProvider.GetService(); 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); @@ -271,6 +271,7 @@ 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(); @@ -278,7 +279,7 @@ public void AddTwilioClient_With_HttpClientProvider_Should_Use_HttpClient() var twilioRestClient = scope.ServiceProvider.GetService(); 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); diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs index c2358d0..78ad6e7 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs @@ -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; @@ -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(), twiml); + var twiml = new VoiceResponse().Say("Ahoy!"); + var result = Mock.Of().TwiML(twiml); var actionContext = CreateActionContext(); await result.ExecuteResultAsync(actionContext); @@ -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().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(), twiml); + var twiml = new MessagingResponse().Message("Ahoy!"); + var result = Mock.Of().TwiML(twiml); var actionContext = CreateActionContext(); await result.ExecuteResultAsync(actionContext); diff --git a/src/Twilio.AspNet.Core.UnitTests/ValidateRequestAttributeTests.cs b/src/Twilio.AspNet.Core.UnitTests/ValidateRequestAttributeTests.cs index c25fde6..774cbad 100644 --- a/src/Twilio.AspNet.Core.UnitTests/ValidateRequestAttributeTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/ValidateRequestAttributeTests.cs @@ -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; @@ -128,13 +126,13 @@ public void ValidateRequestAttribute_Validates_Request_Successfully() var actionExecutingContext = new ActionExecutingContext( new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()), new List(), - new Dictionary(), + new Dictionary(), new object() ); attribute.OnActionExecuting(actionExecutingContext); - Assert.Equal(null, actionExecutingContext.Result); + Assert.Null(actionExecutingContext.Result); } [Fact] @@ -156,13 +154,13 @@ public void ValidateRequestFilter_Validates_Request_Forbid() var actionExecutingContext = new ActionExecutingContext( new ActionContext(fakeContext, new RouteData(), new ActionDescriptor()), new List(), - new Dictionary(), + new Dictionary(), 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); } diff --git a/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestFilterTests.cs b/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestFilterTests.cs index ab0246d..bfd2279 100644 --- a/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestFilterTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestFilterTests.cs @@ -75,7 +75,7 @@ public async Task ValidateRequestFilter_Validates_Request_Successfully() var result = await filter.InvokeAsync( new DefaultEndpointFilterInvocationContext(fakeContext), - _ => ValueTask.FromResult(Results.Ok()) + _ => ValueTask.FromResult(Results.Ok()) ); Assert.IsType(result); @@ -103,7 +103,7 @@ public async Task ValidateRequestFilter_Validates_Request_Forbid() var result = await filter.InvokeAsync( new DefaultEndpointFilterInvocationContext(fakeContext), - _ => ValueTask.FromResult(Results.Ok()) + _ => ValueTask.FromResult(Results.Ok()) ); var statusCodeResult = (StatusCodeHttpResult)result!; diff --git a/src/Twilio.AspNet.Core/HttpRequestExtensions.cs b/src/Twilio.AspNet.Core/HttpRequestExtensions.cs deleted file mode 100644 index 1c0431d..0000000 --- a/src/Twilio.AspNet.Core/HttpRequestExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Http; - -namespace Twilio.AspNet.Core -{ - public static class HttpRequestExtensions - { - public static bool IsLocal(this HttpRequest req) - { - if (req.Headers.ContainsKey("X-Forwarded-For")) - { - // Assume not local if we're behind a proxy - return false; - } - - var connection = req.HttpContext.Connection; - if (connection.RemoteIpAddress != null) - { - if (connection.LocalIpAddress != null) - { - return connection.RemoteIpAddress.Equals(connection.LocalIpAddress); - } - return IPAddress.IsLoopback(connection.RemoteIpAddress); - } - - // for in memory TestServer or when dealing with default connection info - if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) - { - return true; - } - - return false; - } - } -} diff --git a/src/Twilio.AspNet.Core/HttpStatusCodeResult.cs b/src/Twilio.AspNet.Core/HttpStatusCodeResult.cs deleted file mode 100644 index da36eec..0000000 --- a/src/Twilio.AspNet.Core/HttpStatusCodeResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Twilio.AspNet.Core -{ - /// - /// Returns an HTTP status code - /// - public class HttpStatusCodeResult : IActionResult - { - /// - /// Creates a new instance of the class with a specific status code - /// - /// The status code to return - public HttpStatusCodeResult(HttpStatusCode statusCode) - { - StatusCode = (int)statusCode; - } - - /// - /// Gets the status code for this instance - /// - public int StatusCode { get; private set; } - - public Task ExecuteResultAsync(ActionContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - context.HttpContext.Response.StatusCode = StatusCode; - - return Task.CompletedTask; - } - } -} diff --git a/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs b/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs index cd77532..ede4aa3 100644 --- a/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs +++ b/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs @@ -2,8 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -// ReSharper disable once CheckNamespace -namespace Twilio.AspNet.Core.MinimalApi; +namespace Twilio.AspNet.Core; /// /// Adds extension methods to Results.Extensions to write TwiML objects to the HTTP response body @@ -19,38 +18,17 @@ public static class ResultsExtensions // ReSharper disable once InconsistentNaming // ReSharper disable once UnusedParameter.Global public static TwiMLResult TwiML(this IResultExtensions results, TwiML.TwiML twimlResponse) - => new TwiMLResult(twimlResponse); + => new(twimlResponse); } /// /// Writes TwiML object to the HTTP response body /// -// ReSharper disable once InconsistentNaming -public class TwiMLResult : IResult +public partial class TwiMLResult : IResult { - // ReSharper disable once InconsistentNaming - private string twiML; - - /// - /// Creates a TwiMLResult object - /// - /// The TwiML to write to the HTTP response body - // ReSharper disable once InconsistentNaming - public TwiMLResult(TwiML.TwiML twimlResponse) - { - twiML = twimlResponse?.ToString(); - } - /// /// Writes the TwiML to the HTTP response body /// /// The HttpContext containing the Response to write the TwiML to - public Task ExecuteAsync(HttpContext httpContext) - { - twiML ??= ""; - - httpContext.Response.ContentType = "application/xml"; - httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(twiML); - return httpContext.Response.WriteAsync(twiML); - } + public Task ExecuteAsync(HttpContext httpContext) => WriteTwiMLToResponse(httpContext.Response); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/RequestValidationHelper.cs b/src/Twilio.AspNet.Core/RequestValidationHelper.cs index 7cf551b..192fd92 100644 --- a/src/Twilio.AspNet.Core/RequestValidationHelper.cs +++ b/src/Twilio.AspNet.Core/RequestValidationHelper.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Net; using Microsoft.AspNetCore.Http; using Twilio.Security; @@ -18,9 +19,7 @@ public static class RequestValidationHelper /// AuthToken for the account used to sign the request /// Skip validation for local requests public static bool IsValidRequest(HttpContext context, string authToken, bool allowLocal = true) - { - return IsValidRequest(context, authToken, null, allowLocal); - } + => IsValidRequest(context, authToken, null, allowLocal); /// /// Performs request validation using the current HTTP context passed in manually or from @@ -30,11 +29,16 @@ public static bool IsValidRequest(HttpContext context, string authToken, bool al /// AuthToken for the account used to sign the request /// The URL to use for validation, if different from Request.Url (sometimes needed if web site is behind a proxy or load-balancer) /// Skip validation for local requests - public static bool IsValidRequest(HttpContext context, string authToken, string urlOverride, bool allowLocal = true) + public static bool IsValidRequest( + HttpContext context, + string authToken, + string urlOverride, + bool allowLocal = true + ) { var request = context.Request; - if (allowLocal && request.IsLocal()) + if (allowLocal && IsLocal(request)) { return true; } @@ -46,10 +50,10 @@ public static bool IsValidRequest(HttpContext context, string authToken, string ? $"{request.Scheme}://{(request.IsHttps ? request.Host.Host : request.Host.ToUriComponent())}{request.Path}{request.QueryString}" : urlOverride; - var parameters = request.HasFormContentType ? - request.Form.Keys.ToDictionary(k => k, k => request.Form[k].ToString()) + var parameters = request.HasFormContentType + ? request.Form.Keys.ToDictionary(k => k, k => request.Form[k].ToString()) : null; - + var validator = new RequestValidator(authToken); return validator.Validate( url: fullUrl, @@ -57,5 +61,33 @@ public static bool IsValidRequest(HttpContext context, string authToken, string expected: request.Headers["X-Twilio-Signature"] ); } + + private static bool IsLocal(HttpRequest req) + { + if (req.Headers.ContainsKey("X-Forwarded-For")) + { + // Assume not local if we're behind a proxy + return false; + } + + var connection = req.HttpContext.Connection; + if (connection.RemoteIpAddress != null) + { + if (connection.LocalIpAddress != null) + { + return connection.RemoteIpAddress.Equals(connection.LocalIpAddress); + } + + return IPAddress.IsLoopback(connection.RemoteIpAddress); + } + + // for in memory TestServer or when dealing with default connection info + if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) + { + return true; + } + + return false; + } } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwiMLExtensions.cs b/src/Twilio.AspNet.Core/TwiMLExtensions.cs new file mode 100644 index 0000000..9e9f122 --- /dev/null +++ b/src/Twilio.AspNet.Core/TwiMLExtensions.cs @@ -0,0 +1,23 @@ +using Twilio.TwiML; + +namespace Twilio.AspNet.Core +{ + public static class TwiMLExtensions + { + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + public static TwiMLResult ToTwiMLResult(this VoiceResponse voiceResponse) + => new TwiMLResult(voiceResponse); + + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + public static TwiMLResult ToTwiMLResult(this MessagingResponse messagingResponse) + => new TwiMLResult(messagingResponse); + } +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwiMLResult.cs b/src/Twilio.AspNet.Core/TwiMLResult.cs index 7e8f70d..9f6f648 100644 --- a/src/Twilio.AspNet.Core/TwiMLResult.cs +++ b/src/Twilio.AspNet.Core/TwiMLResult.cs @@ -1,40 +1,48 @@ using System.Threading.Tasks; +using System.Xml.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Twilio.AspNet.Core { - // ReSharper disable once InconsistentNaming - public class TwiMLResult : IActionResult + /// + /// TwiMLResult writes TwiML to the HTTP response body + /// + public partial class TwiMLResult : IActionResult { - public string Data { get; protected set; } + private readonly TwiML.TwiML twiml; + private readonly SaveOptions formattingOptions; - public TwiMLResult() + /// The TwiML to respond with + public TwiMLResult(TwiML.TwiML twiml) : this(twiml, SaveOptions.None) { } - - public TwiMLResult(string twiml) - { - Data = twiml; - } - - public TwiMLResult(TwiML.TwiML response) + + /// The TwiML to respond with + /// Specifies how to format TwiML + public TwiMLResult(TwiML.TwiML twiml, SaveOptions formattingOptions) { - if (response != null) - Data = response.ToString(); + this.twiml = twiml; + this.formattingOptions = formattingOptions; } public async Task ExecuteResultAsync(ActionContext actionContext) { var response = actionContext.HttpContext.Response; + await WriteTwiMLToResponse(response); + } + + private async Task WriteTwiMLToResponse(HttpResponse response) + { response.ContentType = "application/xml"; - - if (Data == null) + if (twiml == null) { - Data = ""; + await response.WriteAsync(""); + return; } - await response.WriteAsync(Data); + var data = twiml.ToString(formattingOptions); + await response.WriteAsync(data); } } } diff --git a/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs b/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs index 0604728..66be67d 100644 --- a/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs +++ b/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs @@ -105,19 +105,15 @@ private static void SanitizeTwilioClientOptions(TwilioClientOptions options) { if (isApiKeyConfigured) options.CredentialType = CredentialType.ApiKey; else if (isAuthTokenConfigured) options.CredentialType = CredentialType.AuthToken; - else - throw new Exception( - "Twilio:Client:CredentialType could not be determined. Configure as ApiKey or AuthToken."); + else throw new Exception("Twilio:Client:CredentialType could not be determined. Configure as ApiKey or AuthToken."); } else if (options.CredentialType == CredentialType.ApiKey && !isApiKeyConfigured) { - throw new Exception( - "Twilio:Client:{AccountSid|ApiKeySid|ApiKeySecret} configuration required for CredentialType.ApiKey."); + throw new Exception("Twilio:Client:{AccountSid|ApiKeySid|ApiKeySecret} configuration required for CredentialType.ApiKey."); } else if (options.CredentialType == CredentialType.AuthToken && !isAuthTokenConfigured) { - throw new Exception( - "Twilio:Client:{AccountSid|AuthToken} configuration required for CredentialType.AuthToken."); + throw new Exception("Twilio:Client:{AccountSid|AuthToken} configuration required for CredentialType.AuthToken."); } } diff --git a/src/Twilio.AspNet.Core/TwilioController.cs b/src/Twilio.AspNet.Core/TwilioController.cs index 73d6ac0..f6baea7 100644 --- a/src/Twilio.AspNet.Core/TwilioController.cs +++ b/src/Twilio.AspNet.Core/TwilioController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Xml.Linq; +using Microsoft.AspNetCore.Mvc; using Twilio.TwiML; namespace Twilio.AspNet.Core @@ -14,22 +15,34 @@ public class TwilioController : ControllerBase /// /// [NonAction] - // ReSharper disable once InconsistentNaming - public TwiMLResult TwiML(MessagingResponse response) - { - return new TwiMLResult(response); - } + public TwiMLResult TwiML(MessagingResponse response) => new TwiMLResult(response); + + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + [NonAction] + public TwiMLResult TwiML(MessagingResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); + + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + [NonAction] + public TwiMLResult TwiML(VoiceResponse response) => new TwiMLResult(response); /// /// Returns a properly formatted TwiML response /// /// + /// /// [NonAction] - // ReSharper disable once InconsistentNaming - public TwiMLResult TwiML(VoiceResponse response) - { - return new TwiMLResult(response); - } + public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs b/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs index e100786..894e955 100644 --- a/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs +++ b/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Xml.Linq; +using Microsoft.AspNetCore.Mvc; using Twilio.TwiML; namespace Twilio.AspNet.Core @@ -14,9 +15,18 @@ public static class TwilioControllerExtensions /// /// /// - // ReSharper disable once InconsistentNaming public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) => new TwiMLResult(response); + + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); /// /// Returns a properly formatted TwiML response @@ -24,8 +34,17 @@ public static TwiMLResult TwiML(this ControllerBase controller, MessagingRespons /// /// /// - // ReSharper disable once InconsistentNaming public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response) => new TwiMLResult(response); + + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs b/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs index ba4d9f7..a4189ce 100644 --- a/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs +++ b/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -66,7 +67,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) if (!RequestValidationHelper.IsValidRequest(httpContext, AuthToken, urlOverride, AllowLocal)) { - filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden); + filterContext.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden); } base.OnActionExecuting(filterContext); diff --git a/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs b/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs new file mode 100644 index 0000000..5a8bb9f --- /dev/null +++ b/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Web; +using System.Web.Mvc; +using Twilio.TwiML; + +namespace Twilio.AspNet.Mvc.UnitTests +{ + public class ContextMocks + { + public Moq.Mock HttpContext { get; set; } + public Moq.Mock Request { get; set; } + public Moq.Mock Response { get; set; } + public Moq.Mock ControllerContext { get; set; } + + public ContextMocks(bool isLocal, NameValueCollection form = null) : this("", isLocal, form) + { + } + + public ContextMocks(string urlOverride, bool isLocal, NameValueCollection form = null) + { + var headers = new NameValueCollection(); + headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form)); + + HttpContext = new Moq.Mock(); + Request = new Moq.Mock(); + Response = new Moq.Mock(); + ControllerContext = new Moq.Mock(); + + HttpContext.Setup(x => x.Request).Returns(Request.Object); + HttpContext.Setup(x => x.Response).Returns(Response.Object); + ControllerContext.Setup(x => x.HttpContext).Returns(HttpContext.Object); + + Request.Setup(x => x.IsLocal).Returns(isLocal); + Request.Setup(x => x.Headers).Returns(headers); + Request.Setup(x => x.Url).Returns(new Uri(ContextMocks.fakeUrl)); + if (form != null) + { + Request.Setup(x => x.HttpMethod).Returns("POST"); + Request.Setup(x => x.Form).Returns(form); + } + + Response.Setup(x => x.Output).Returns(new Utf8StringWriter()); + } + + public static string fakeUrl = "https://api.example.com/webhook"; + public static string fakeAuthToken = "thisisafakeauthtoken"; + + private string CalculateSignature(string urlOverride, NameValueCollection form) + { + var value = new StringBuilder(); + value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride); + + if (form != null) + { + var sortedKeys = form.AllKeys.OrderBy(k => k, StringComparer.Ordinal).ToList(); + foreach (var key in sortedKeys) + { + value.Append(key); + value.Append(form[key]); + } + } + + var sha1 = new HMACSHA1(Encoding.UTF8.GetBytes(ContextMocks.fakeAuthToken)); + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(value.ToString())); + + return Convert.ToBase64String(hash); + } + } +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs b/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs index 9cfdd84..8610945 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs +++ b/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs @@ -1,66 +1,8 @@ -using System; using System.Collections.Specialized; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Web; using Xunit; namespace Twilio.AspNet.Mvc.UnitTests { - public class ContextMocks - { - public Moq.Mock HttpContext { get; set; } - public Moq.Mock Request { get; set; } - - public ContextMocks(bool isLocal, NameValueCollection form = null) : this("", isLocal, form) - { - } - - public ContextMocks(string urlOverride, bool isLocal, NameValueCollection form = null) - { - var headers = new NameValueCollection(); - headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form)); - - HttpContext = new Moq.Mock(); - Request = new Moq.Mock(); - HttpContext.Setup(x => x.Request).Returns(Request.Object); - Request.Setup(x => x.IsLocal).Returns(isLocal); - Request.Setup(x => x.Headers).Returns(headers); - Request.Setup(x => x.Url).Returns(new Uri(ContextMocks.fakeUrl)); - - if (form != null) - { - Request.Setup(x => x.HttpMethod).Returns("POST"); - Request.Setup(x => x.Form).Returns(form); - } - } - - public static string fakeUrl = "https://api.example.com/webhook"; - public static string fakeAuthToken = "thisisafakeauthtoken"; - - private string CalculateSignature(string urlOverride, NameValueCollection form) - { - var value = new StringBuilder(); - value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride); - - if (form != null) - { - var sortedKeys = form.AllKeys.OrderBy(k => k, StringComparer.Ordinal).ToList(); - foreach (var key in sortedKeys) - { - value.Append(key); - value.Append(form[key]); - } - } - - var sha1 = new HMACSHA1(Encoding.UTF8.GetBytes(ContextMocks.fakeAuthToken)); - var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(value.ToString())); - - return Convert.ToBase64String(hash); - } - } - public class RequestValidationHelperTests { [Fact] diff --git a/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs b/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs index 284aa24..91ae7a8 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs +++ b/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs @@ -1,93 +1,77 @@ -using System.Text; -using System.Xml; +using System; +using System.IO; +using System.Xml.Linq; +using Twilio.TwiML; using Xunit; namespace Twilio.AspNet.Mvc.UnitTests { - // ReSharper disable once InconsistentNaming - public class TwiMLResultTests + public class TwiMLResultTest { - public const string AsciiChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - public const string UnicodeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890äåéö"; + private readonly ContextMocks mocks = new ContextMocks(true); + private static readonly string NewLine = Environment.NewLine; - //string constructor [Fact] - public void TestStringDefaultEncodingPass() + public void TestVoiceResponse() { - var responseString = GetVoiceResponse(UnicodeChars).ToString(); - - var result = new TwiMLResult(responseString); - - Assert.Contains(UnicodeChars, result.Data.ToString()); - } - - [Fact] - public void TestStringAsciiEncodingFail() - { - var responseString = GetVoiceResponse(UnicodeChars).ToString(); - - var result = new TwiMLResult(responseString, Encoding.ASCII); - - Assert.Contains(AsciiChars, result.Data.ToString()); - Assert.DoesNotContain(UnicodeChars, result.Data.ToString()); - } - - [Fact] - public void TestStringUnicodeEncodingUtf8() - { - var responseString = GetVoiceResponse(UnicodeChars).ToString(); - - var result = new TwiMLResult(responseString, Encoding.UTF8); - - Assert.Contains(UnicodeChars, result.Data.ToString()); - } - - //voice response constructor - [Fact] - public void TestVoiceResponseDefaultEncodingPass() - { - var response = GetVoiceResponse(UnicodeChars); + var response = new VoiceResponse().Say("Ahoy!"); var result = new TwiMLResult(response); - - Assert.Contains(UnicodeChars, result.Data.ToString()); + result.ExecuteResult(mocks.ControllerContext.Object); + + Assert.Equal($"{NewLine}" + + $"{NewLine}" + + $" Ahoy!{NewLine}" + + "", + mocks.Response.Object.Output.ToString() + ); } [Fact] - public void TestVoiceResponseAsciiEncodingFail() + public void TestVoiceResponseUnformatted() { - var response = GetVoiceResponse(UnicodeChars); + var response = new VoiceResponse().Say("Ahoy!"); - var result = new TwiMLResult(response, Encoding.ASCII); + var result = new TwiMLResult(response, SaveOptions.DisableFormatting); + result.ExecuteResult(mocks.ControllerContext.Object); - Assert.Contains(AsciiChars, result.Data.ToString()); - Assert.DoesNotContain(UnicodeChars, result.Data.ToString()); + Assert.Equal("" + + "" + + "Ahoy!" + + "", + mocks.Response.Object.Output.ToString() + ); } [Fact] - public void TestVoiceResponseUnicodeEncodingUtf8() + public void TestVoiceResponseUnformattedUtf16() { - var response = GetVoiceResponse(UnicodeChars); - - var result = new TwiMLResult(response, Encoding.UTF8); - - Assert.Contains(UnicodeChars, result.Data.ToString()); + // string writer has Utf16 encoding + mocks.Response.Setup(r => r.Output).Returns(new StringWriter()); + var response = new VoiceResponse().Say("Ahoy!"); + + var result = new TwiMLResult(response, SaveOptions.DisableFormatting); + result.ExecuteResult(mocks.ControllerContext.Object); + + Assert.Equal("" + + "" + + "Ahoy!" + + "", + mocks.Response.Object.Output.ToString() + ); } [Fact] - public void TestVoiceResponseExplicitDefaultEncodingFail() - { - var response = GetVoiceResponse(UnicodeChars); - - Assert.Throws(() => - { - new TwiMLResult(response, Encoding.Default); - }); - } - - public TwiML.VoiceResponse GetVoiceResponse(string content) + public void TestNullTwiml() { - return new TwiML.VoiceResponse().Say(content); + var result = new TwiMLResult(null); + result.ExecuteResult(mocks.ControllerContext.Object); + + Assert.Equal( + "", + mocks.Response.Object.Output.ToString() + ); + mocks.Response.Object.Output.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc.UnitTests/TwilioControllerTests.cs b/src/Twilio.AspNet.Mvc.UnitTests/TwilioControllerTests.cs deleted file mode 100644 index 9a3da92..0000000 --- a/src/Twilio.AspNet.Mvc.UnitTests/TwilioControllerTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Xunit; - -namespace Twilio.AspNet.Mvc.UnitTests -{ - public class TwilioControllerTests - { - [Fact] - public void TestVoiceResponseDefaultEncodingPass() - { - var response = GetVoiceResponse(TwiMLResultTests.UnicodeChars); - - var result = new TwilioController().TwiML(response); - - Assert.Contains(TwiMLResultTests.UnicodeChars, result.Data.ToString()); - } - - [Fact] - public void TestMessagingResponseDefaultEncodingPass() - { - var response = GetMessagingResponse(TwiMLResultTests.UnicodeChars); - - var result = new TwilioController().TwiML(response); - - Assert.Contains(TwiMLResultTests.UnicodeChars, result.Data.ToString()); - } - - private static TwiML.VoiceResponse GetVoiceResponse(string content) - { - return new TwiML.VoiceResponse().Say(content); - } - - private static TwiML.MessagingResponse GetMessagingResponse(string content) - { - return new TwiML.MessagingResponse().Message(content); - } - } -} diff --git a/src/Twilio.AspNet.Mvc/HttpStatusCodeResult.cs b/src/Twilio.AspNet.Mvc/HttpStatusCodeResult.cs deleted file mode 100644 index 96f87c3..0000000 --- a/src/Twilio.AspNet.Mvc/HttpStatusCodeResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Net; -using System.Web.Mvc; - -namespace Twilio.AspNet.Mvc -{ - /// - /// Returns an HTTP status code - /// - public class HttpStatusCodeResult : ActionResult - { - /// - /// Creates a new instance of the class with a specific status code - /// - /// The status code to return - public HttpStatusCodeResult(HttpStatusCode statusCode) - { - StatusCode = (int)statusCode; - } - - /// - /// Gets the status code for this instance - /// - public int StatusCode { get; private set; } - - public override void ExecuteResult(ControllerContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - context.HttpContext.Response.StatusCode = StatusCode; - } - } -} diff --git a/src/Twilio.AspNet.Mvc/TwiMLResult.cs b/src/Twilio.AspNet.Mvc/TwiMLResult.cs index 93d6d77..76296bd 100644 --- a/src/Twilio.AspNet.Mvc/TwiMLResult.cs +++ b/src/Twilio.AspNet.Mvc/TwiMLResult.cs @@ -1,69 +1,42 @@ -using System.IO; -using System.Text; -using System.Web.Mvc; -using System.Xml; +using System.Web.Mvc; using System.Xml.Linq; namespace Twilio.AspNet.Mvc { - // ReSharper disable once InconsistentNaming public class TwiMLResult : ActionResult { - public XDocument Data { get; protected set; } + private readonly SaveOptions formattingOptions; + private readonly TwiML.TwiML dataTwiml; - public TwiMLResult() + public TwiMLResult(TwiML.TwiML response) : this(response, SaveOptions.None) { } - public TwiMLResult(string twiml) + public TwiMLResult(TwiML.TwiML response, SaveOptions formattingOptions) { - Data = LoadFromString(twiml, Encoding.UTF8); - } - - public TwiMLResult(string twiml, Encoding encoding) - { - Data = LoadFromString(twiml, encoding); - } - - public TwiMLResult(XDocument twiml) - { - Data = twiml; - } - - public TwiMLResult(TwiML.TwiML response) - { - if (response != null) - Data = LoadFromString(response.ToString(), Encoding.UTF8); - } - - public TwiMLResult(TwiML.TwiML response, Encoding encoding) - { - if (response != null) - Data = LoadFromString(response.ToString(), encoding); - } - - private static XDocument LoadFromString(string twiml, Encoding encoding) - { - var stream = new MemoryStream(encoding.GetBytes(twiml)); - - var settings = new XmlReaderSettings(); - settings.DtdProcessing = DtdProcessing.Prohibit; - - var reader = XmlReader.Create(stream, settings); - return XDocument.Load(reader); + this.dataTwiml = response; + this.formattingOptions = formattingOptions; } public override void ExecuteResult(ControllerContext controllerContext) { - var context = controllerContext.RequestContext.HttpContext; - context.Response.ContentType = "application/xml"; + var response = controllerContext.HttpContext.Response; + var encoding = response.Output.Encoding.BodyName; + response.ContentType = "application/xml"; + + if (dataTwiml == null) + { + response.Output.Write($""); + return; + } - if (Data == null) + var twimlString = dataTwiml.ToString(formattingOptions); + if (encoding != "utf-8") { - Data = new XDocument(new XElement("Response")); + twimlString = twimlString.Replace("utf-8", encoding); } - Data.Save(context.Response.Output); + response.Output.Write(twimlString); } } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs b/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs index 8495be5..729820f 100644 --- a/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs +++ b/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs @@ -1,5 +1,4 @@ -using System; -using System.Configuration; +using System.Configuration; namespace Twilio.AspNet.Mvc { diff --git a/src/Twilio.AspNet.Mvc/TwilioController.cs b/src/Twilio.AspNet.Mvc/TwilioController.cs index 880504a..6a90921 100644 --- a/src/Twilio.AspNet.Mvc/TwilioController.cs +++ b/src/Twilio.AspNet.Mvc/TwilioController.cs @@ -1,5 +1,5 @@ -using System.Text; -using System.Web.Mvc; +using System.Web.Mvc; +using System.Xml.Linq; using Twilio.TwiML; namespace Twilio.AspNet.Mvc @@ -14,45 +14,33 @@ public class TwilioController : Controller /// /// /// - // ReSharper disable once InconsistentNaming public TwiMLResult TwiML(MessagingResponse response) - { - return new TwiMLResult(response); - } - + => new TwiMLResult(response); + /// /// Returns a properly formatted TwiML response /// /// - /// Encoding to use for Xml + /// /// - // ReSharper disable once InconsistentNaming - public TwiMLResult TwiML(MessagingResponse response, Encoding encoding) - { - return new TwiMLResult(response, encoding); - } + public TwiMLResult TwiML(MessagingResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); /// /// Returns a properly formatted TwiML response /// /// /// - // ReSharper disable once InconsistentNaming public TwiMLResult TwiML(VoiceResponse response) - { - return new TwiMLResult(response); - } + => new TwiMLResult(response); /// /// Returns a properly formatted TwiML response /// /// - /// Encoding to use for Xml + /// /// - // ReSharper disable once InconsistentNaming - public TwiMLResult TwiML(VoiceResponse response, Encoding encoding) - { - return new TwiMLResult(response, encoding); - } - } + public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); + } } diff --git a/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs b/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs index 580c2ec..06bc669 100644 --- a/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs +++ b/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs @@ -1,5 +1,5 @@ -using System.Text; -using System.Web.Mvc; +using System.Web.Mvc; +using System.Xml.Linq; using Twilio.TwiML; namespace Twilio.AspNet.Mvc @@ -15,20 +15,18 @@ public static class TwilioControllerExtensions /// /// /// - // ReSharper disable once InconsistentNaming public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) => new TwiMLResult(response); - + /// /// Returns a properly formatted TwiML response /// /// /// - /// Encoding to use for Xml + /// /// - // ReSharper disable once InconsistentNaming - public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response, Encoding encoding) - => new TwiMLResult(response, encoding); + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); /// /// Returns a properly formatted TwiML response @@ -36,19 +34,17 @@ public static TwiMLResult TwiML(this ControllerBase controller, MessagingRespons /// /// /// - // ReSharper disable once InconsistentNaming public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response) => new TwiMLResult(response); - + /// /// Returns a properly formatted TwiML response /// /// /// - /// Encoding to use for Xml + /// /// - // ReSharper disable once InconsistentNaming - public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, Encoding encoding) - => new TwiMLResult(response, encoding); + public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) + => new TwiMLResult(response, formattingOptions); } } diff --git a/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs b/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs index b25abbc..d022f6f 100644 --- a/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs +++ b/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs @@ -6,8 +6,6 @@ namespace Twilio.AspNet.Mvc public static class TwilioUriHelper { public static Uri ActionUri(this UrlHelper helper, string actionName, string controllerName) - { - return new Uri(helper.Action(actionName, controllerName), UriKind.Relative); - } + => new Uri(helper.Action(actionName, controllerName), UriKind.Relative); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.sln.DotSettings b/src/Twilio.AspNet.sln.DotSettings new file mode 100644 index 0000000..830911e --- /dev/null +++ b/src/Twilio.AspNet.sln.DotSettings @@ -0,0 +1,2 @@ + + ML \ No newline at end of file diff --git a/src/testapps/AspNetCore/Program.cs b/src/testapps/AspNetCore/Program.cs index ec7ac8a..cb5308a 100644 --- a/src/testapps/AspNetCore/Program.cs +++ b/src/testapps/AspNetCore/Program.cs @@ -1,5 +1,4 @@ using Twilio.AspNet.Core; -using Twilio.AspNet.Core.MinimalApi; using Twilio.TwiML; var builder = WebApplication.CreateBuilder(args);