diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d5e5b8..28b9727 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: default: 0.0.0-alpha required: false type: string - + push: branches: [ "main" ] paths: @@ -30,11 +30,11 @@ on: description: 'The version of the library to use when compiling and packaging.' required: true -env: +env: CI: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_NOLOGO: true - + jobs: build: name: Build, test, and pack @@ -42,13 +42,12 @@ jobs: permissions: checks: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x - dotnet-quality: ga + dotnet-version: 9.x - name: Update project versions run: | @@ -62,67 +61,59 @@ jobs: } } shell: pwsh - + # Build and pack Twilio.AspNet.Common - name: (Twilio.AspNet.Common) Restore run: dotnet restore working-directory: src/Twilio.AspNet.Common/ shell: pwsh - + - name: (Twilio.AspNet.Common) Build run: dotnet build --no-restore --configuration Release working-directory: src/Twilio.AspNet.Common/ shell: pwsh - + - name: (Twilio.AspNet.Common) Pack run: dotnet pack -c Release -o ..\..\ working-directory: src/Twilio.AspNet.Common/ shell: pwsh - name: (Twilio.AspNet.Common) Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Twilio.AspNet.Common NuGet Package path: | Twilio.AspNet.Common.${{ inputs.libraryVersion || '0.0.0-alpha' }}.nupkg Twilio.AspNet.Common.${{ inputs.libraryVersion || '0.0.0-alpha' }}.snupkg - + # Build, test, and pack Twilio.AspNet.Core - name: (Twilio.AspNet.Core) Restore run: dotnet restore working-directory: src/Twilio.AspNet.Core/ shell: pwsh - + - name: (Twilio.AspNet.Core) Build run: dotnet build --no-restore --configuration Release working-directory: src/Twilio.AspNet.Core/ shell: pwsh - + - name: (Twilio.AspNet.Core.UnitTests) Restore run: dotnet restore working-directory: src/Twilio.AspNet.Core.UnitTests/ shell: pwsh - + - name: (Twilio.AspNet.Core.UnitTests) Build run: dotnet build --no-restore working-directory: src/Twilio.AspNet.Core.UnitTests/ shell: pwsh - + - name: (Twilio.AspNet.Core.UnitTests) Test - run: dotnet test --no-build --logger trx + run: dotnet test --no-build --no-restore working-directory: src/Twilio.AspNet.Core.UnitTests/ shell: pwsh - - name: (Twilio.AspNet.Core.UnitTests) Report Tests - uses: dorny/test-reporter@v1 - if: success() || failure() # run this step even if previous step failed - with: - name: Twilio.AspNet.Core.UnitTests - path: src/Twilio.AspNet.Core.UnitTests/TestResults/*.trx - reporter: dotnet-trx - - name: (Twilio.AspNet.Core) Pack - run: dotnet pack -c Release -o ..\..\ + run: dotnet pack --no-build --no-restore -c Release -o ..\..\ working-directory: src/Twilio.AspNet.Core/ shell: pwsh @@ -133,48 +124,40 @@ jobs: path: | Twilio.AspNet.Core.${{ inputs.libraryVersion || '0.0.0-alpha' }}.nupkg Twilio.AspNet.Core.${{ inputs.libraryVersion || '0.0.0-alpha' }}.snupkg - + # Build, test, and pack Twilio.AspNet.Mvc - name: (Twilio.AspNet.Mvc) Restore run: dotnet restore working-directory: src/Twilio.AspNet.Mvc/ shell: pwsh - + - name: (Twilio.AspNet.Mvc) Build run: dotnet build --no-restore --configuration Release working-directory: src/Twilio.AspNet.Mvc/ shell: pwsh - + - name: (Twilio.AspNet.Mvc.UnitTests) Restore run: dotnet restore working-directory: src/Twilio.AspNet.Mvc.UnitTests/ shell: pwsh - + - name: (Twilio.AspNet.Mvc.UnitTests) Build run: dotnet build --no-restore working-directory: src/Twilio.AspNet.Mvc.UnitTests/ shell: pwsh - + - name: (Twilio.AspNet.Mvc.UnitTests) Test - run: dotnet test --no-build --logger trx + run: dotnet test --no-build --no-restore working-directory: src/Twilio.AspNet.Mvc.UnitTests/ shell: pwsh - - name: (Twilio.AspNet.Mvc.UnitTests) Report Tests - uses: dorny/test-reporter@v1 - if: success() || failure() # run this step even if previous step failed - with: - name: Twilio.AspNet.Mvc.UnitTests - path: src/Twilio.AspNet.Mvc.UnitTests/TestResults/*.trx - reporter: dotnet-trx - - name: (Twilio.AspNet.Mvc) Pack - run: dotnet pack -c Release -o ..\..\ + run: dotnet pack --no-build --no-restore -c Release -o ..\..\ working-directory: src/Twilio.AspNet.Mvc/ shell: pwsh - + - name: (Twilio.AspNet.Mvc) Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Twilio.AspNet.Mvc NuGet Package path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aea65e4..98b9d4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ env: jobs: build: uses: ./.github/workflows/ci.yml - if: contains('["Swimburger","dprothero"]', github.actor) + if: contains('["Swimburger","dprothero","AJLange"]', github.actor) name: Build, test, and pack permissions: checks: write @@ -29,21 +29,21 @@ jobs: secrets: inherit release: - if: contains('["Swimburger","dprothero"]', github.actor) + if: contains('["Swimburger","dprothero","AJLange"]', github.actor) runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 name: Download Twilio.AspNet.Common NuGet Package with: name: Twilio.AspNet.Common NuGet Package - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 name: Download Twilio.AspNet.Core NuGet Package with: name: Twilio.AspNet.Core NuGet Package - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 name: Download Twilio.AspNet.Mvc NuGet Package with: name: Twilio.AspNet.Mvc NuGet Package diff --git a/README.md b/README.md index 1a49fd8..6c9c249 100644 --- a/README.md +++ b/README.md @@ -552,7 +552,7 @@ bool IsValidTwilioRequest(HttpContext httpContext) ?.Value ?? throw new Exception("TwilioRequestValidationOptions missing."); string? urlOverride = null; - if (options.BaseUrlOverride != null) + if (options.BaseUrlOverride is not null) { var request = httpContext.Request; urlOverride = $"{options.BaseUrlOverride.TrimEnd('/')}{request.Path}{request.QueryString}"; diff --git a/build.ps1 b/build.ps1 old mode 100644 new mode 100755 index 29ba0bb..5d8c5c2 --- a/build.ps1 +++ b/build.ps1 @@ -1,3 +1,4 @@ +#!/usr/bin/env pwsh $originalLocation = Get-Location function Remove-EntirePath() { diff --git a/src/Twilio.AspNet.Common/SmsRequest.cs b/src/Twilio.AspNet.Common/SmsRequest.cs index 380933e..88db385 100644 --- a/src/Twilio.AspNet.Common/SmsRequest.cs +++ b/src/Twilio.AspNet.Common/SmsRequest.cs @@ -1,54 +1,55 @@ -namespace Twilio.AspNet.Common +namespace Twilio.AspNet.Common; + +/// +/// This class can be used as the parameter on your SMS action. Incoming parameters will be bound here. +/// +/// https://www.twilio.com/docs/messaging/guides/webhook-request +public class SmsRequest : TwilioRequest { /// - /// This class can be used as the parameter on your SMS action. Incoming parameters will be bound here. + /// A 34 character unique identifier for the message. May be used to later retrieve this message from the REST API. /// - /// 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. - /// - public string MessageSid { get; set; } - - /// - /// Same value as MessageSid. Deprecated and included for backward compatibility. - /// - public string SmsSid { get; set; } + public string MessageSid { get; set; } = null!; + + /// + /// Same value as MessageSid. Deprecated and included for backward compatibility. + /// + [Obsolete("Use MessageSid instead")] + public string SmsSid { get; set; }= null!; - /// - /// The text body of the SMS message. Up to 160 characters long - /// - public string Body { get; set; } + /// + /// The text body of the SMS message. Up to 160 characters long + /// + public string Body { get; set; }= null!; - /// - /// The status of the message - /// - public string MessageStatus { get; set; } + /// + /// The status of the message + /// + public string MessageStatus { get; set; }= null!; - /// - /// The message OptOut type - /// - public string OptOutType { get; set; } + /// + /// The message OptOut type + /// + public string? OptOutType { get; set; } - /// - /// 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; } + /// + /// A unique identifier of the messaging service + /// + public string? MessagingServiceSid { get; set; } - /// - /// The number of media items associated with a "Click to WhatsApp" advertisement. - /// - public int ReferralNumMedia { get; set; } + /// + /// The number of media items associated with your message + /// + public int NumMedia { get; set; } - /// - /// The number of media files associated with the Message resource - /// - public int NumSegments { get; set; } - } + /// + /// The number of media items associated with a "Click to WhatsApp" advertisement. + /// + public int ReferralNumMedia { get; set; } + + /// + /// The number of media files associated with the Message resource + /// + public int NumSegments { get; set; } + } diff --git a/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs b/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs index 2736dfc..e1b89a6 100644 --- a/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs +++ b/src/Twilio.AspNet.Common/SmsStatusCallbackRequest.cs @@ -1,41 +1,40 @@ -namespace Twilio.AspNet.Common +namespace Twilio.AspNet.Common; + +public class SmsStatusCallbackRequest: SmsRequest { - public class SmsStatusCallbackRequest: SmsRequest - { - /// - /// The error code (if any) associated with your message. If your message - /// 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-resource#delivery-related-errors - /// - public string ErrorCode { get; set; } + /// + /// The error code (if any) associated with your message. If your message + /// 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-resource#delivery-related-errors + /// + public string? ErrorCode { get; set; } - /// - /// The Installed Channel SID (found on the Channel detail page) that was - /// used to send this message. Only present if the message was sent using a - /// Channel. - /// - public string ChannelInstallSid { get; set; } + /// + /// The Installed Channel SID (found on the Channel detail page) that was + /// used to send this message. Only present if the message was sent using a + /// Channel. + /// + public string? ChannelInstallSid { get; set; } - /// - /// The Error message returned by the underlying Channel if Message delivery - /// failed. Only present if the message was sent using a Channel and message - /// delivery failed. - /// - public string ChannelStatusMessage { get; set; } + /// + /// The Error message returned by the underlying Channel if Message delivery + /// failed. Only present if the message was sent using a Channel and message + /// delivery failed. + /// + public string? ChannelStatusMessage { get; set; } - /// - /// Channel specific prefix that allows you to identify which channel this - /// message was sent over. - /// - public string ChannelPrefix { get; set; } + /// + /// Channel specific prefix that allows you to identify which channel this + /// message was sent over. + /// + public string? ChannelPrefix { get; set; } - /// - /// Contains post-delivery events. If the Channel supports Read receipts, this - /// parameter will be included with a value of READ after the user has read - /// the message. Currently supported only for WhatsApp. - /// - public string EventType { get; set; } - } + /// + /// Contains post-delivery events. If the Channel supports Read receipts, this + /// parameter will be included with a value of READ after the user has read + /// the message. Currently supported only for WhatsApp. + /// + public string? EventType { get; set; } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Common/StatusCallbackRequest.cs b/src/Twilio.AspNet.Common/StatusCallbackRequest.cs index d42edb1..d54b6d0 100644 --- a/src/Twilio.AspNet.Common/StatusCallbackRequest.cs +++ b/src/Twilio.AspNet.Common/StatusCallbackRequest.cs @@ -1,18 +1,17 @@ -namespace Twilio.AspNet.Common +namespace Twilio.AspNet.Common; + +/// +/// This class can be used as the parameter on your StatusCallback action. Incoming parameters will be bound here. +/// +/// https://www.twilio.com/docs/voice/twiml#ending-the-call-callback-requests +public class StatusCallbackRequest : VoiceRequest { /// - /// This class can be used as the parameter on your StatusCallback action. Incoming parameters will be bound here. + /// The duration in seconds of the just-completed call. /// - /// https://www.twilio.com/docs/voice/twiml#ending-the-call-callback-requests - public class StatusCallbackRequest : VoiceRequest - { - /// - /// The duration in seconds of the just-completed call. - /// - public float CallDuration { get; set; } + public float CallDuration { get; set; } - public string Called { get; set; } - public string Caller { get; set; } - public float Duration { get; set; } - } -} + public string? Called { get; set; } + public string? Caller { get; set; } + public float Duration { get; set; } +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Common/Twilio.AspNet.Common.csproj b/src/Twilio.AspNet.Common/Twilio.AspNet.Common.csproj index f59f65b..65e4604 100644 --- a/src/Twilio.AspNet.Common/Twilio.AspNet.Common.csproj +++ b/src/Twilio.AspNet.Common/Twilio.AspNet.Common.csproj @@ -1,6 +1,9 @@ netstandard2.0 + 13 + enable + enable Library 0.0.0-alpha Twilio.AspNet.Common @@ -9,11 +12,12 @@ Twilio request classes for handling Twilio webhooks. false Refer to the changelog at https://github.com/twilio-labs/twilio-aspnet/blob/main/CHANGELOG.md - Copyright 2022 (c) Twilio, Inc. All rights reserved. + Copyright 2024 (c) Twilio, Inc. All rights reserved. twilio;twiml;sms;voice;telephony;phone;aspnet Apache-2.0 https://github.com/twilio/twilio-aspnet https://github.com/twilio-labs/twilio-aspnet.git + git https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/twilio-icon-64x64.png icon.png README.md @@ -30,7 +34,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Twilio.AspNet.Common/TwilioRequest.cs b/src/Twilio.AspNet.Common/TwilioRequest.cs index 6e02eaf..dd6efe8 100644 --- a/src/Twilio.AspNet.Common/TwilioRequest.cs +++ b/src/Twilio.AspNet.Common/TwilioRequest.cs @@ -1,69 +1,68 @@ -namespace Twilio.AspNet.Common -{ +namespace Twilio.AspNet.Common; + +/// +/// Base class for mapping incoming request parameters into a strongly typed object +/// +public abstract class TwilioRequest +{ /// - /// Base class for mapping incoming request parameters into a strongly typed object + /// Your Twilio account id. It is 34 characters long, and always starts with the letters AC /// - public abstract class TwilioRequest - { - /// - /// Your Twilio account id. It is 34 characters long, and always starts with the letters AC - /// - public string AccountSid { get; set; } + public string AccountSid { get; set; } = null!; - /// - /// The phone number or client identifier of the party that initiated the call - /// - /// - /// Phone numbers are formatted with a '+' and country code, e.g. +16175551212 (E.164 format). Client identifiers begin with the client: URI scheme; for example, for a call from a client named 'tommy', the From parameter will be client:tommy. - /// - public string From { get; set; } + /// + /// The phone number or client identifier of the party that initiated the call + /// + /// + /// Phone numbers are formatted with a '+' and country code, e.g. +16175551212 (E.164 format). Client identifiers begin with the client: URI scheme; for example, for a call from a client named 'tommy', the From parameter will be client:tommy. + /// + public string From { get; set; } = null!; - /// - /// The phone number or client identifier of the called party - /// - /// - /// Phone numbers are formatted with a '+' and country code, e.g. +16175551212 (E.164 format). Client identifiers begin with the client: URI scheme; for example, for a call to a client named 'jenny', the To parameter will be client:jenny. - /// - public string To { get; set; } + /// + /// The phone number or client identifier of the called party + /// + /// + /// Phone numbers are formatted with a '+' and country code, e.g. +16175551212 (E.164 format). Client identifiers begin with the client: URI scheme; for example, for a call to a client named 'jenny', the To parameter will be client:jenny. + /// + public string To { get; set; } = null!; - /// - /// The city of the caller - /// - public string FromCity { get; set; } + /// + /// The city of the caller + /// + public string? FromCity { get; set; } - /// - /// The state or province of the caller - /// - public string FromState { get; set; } + /// + /// The state or province of the caller + /// + public string? FromState { get; set; } - /// - /// The postal code of the caller - /// - public string FromZip { get; set; } + /// + /// The postal code of the caller + /// + public string? FromZip { get; set; } - /// - /// The country of the caller - /// - public string FromCountry { get; set; } + /// + /// The country of the caller + /// + public string? FromCountry { get; set; } - /// - /// The city of the called party - /// - public string ToCity { get; set; } + /// + /// The city of the called party + /// + public string? ToCity { get; set; } - /// - /// The state or province of the called party - /// - public string ToState { get; set; } + /// + /// The state or province of the called party + /// + public string? ToState { get; set; } - /// - /// The postal code of the called party - /// - public string ToZip { get; set; } + /// + /// The postal code of the called party + /// + public string? ToZip { get; set; } - /// - /// The country of the called party - /// - public string ToCountry { get; set; } - } -} + /// + /// The country of the called party + /// + public string? ToCountry { get; set; } +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Common/VoiceRequest.cs b/src/Twilio.AspNet.Common/VoiceRequest.cs index fa86399..bd5e6e1 100644 --- a/src/Twilio.AspNet.Common/VoiceRequest.cs +++ b/src/Twilio.AspNet.Common/VoiceRequest.cs @@ -1,170 +1,169 @@ -namespace Twilio.AspNet.Common +namespace Twilio.AspNet.Common; + +/// +/// This class can be used as the parameter on your voice action. Incoming parameters will be bound here. +/// +/// https://www.twilio.com/docs/usage/webhooks/voice-webhooks +public class VoiceRequest : TwilioRequest { /// - /// This class can be used as the parameter on your voice action. Incoming parameters will be bound here. - /// - /// https://www.twilio.com/docs/usage/webhooks/voice-webhooks - public class VoiceRequest : TwilioRequest - { - /// - /// A unique identifier for this call, generated by Twilio - /// - public string CallSid { get; set; } - - /// - /// A descriptive status for the call. The value is one of queued, ringing, in-progress, completed, busy, failed or no-answer - /// - public string CallStatus { get; set; } - - /// - /// The version of the Twilio API used to handle this call. For incoming calls, this is determined by the API version set on the called number. For outgoing calls, this is the API version used by the outgoing call's REST API request - /// - public string ApiVersion { get; set; } + /// A unique identifier for this call, generated by Twilio + /// + public string CallSid { get; set; } = null!; + + /// + /// A descriptive status for the call. The value is one of queued, ringing, in-progress, completed, busy, failed or no-answer + /// + public string CallStatus { get; set; } = null!; + + /// + /// The version of the Twilio API used to handle this call. For incoming calls, this is determined by the API version set on the called number. For outgoing calls, this is the API version used by the outgoing call's REST API request + /// + public string ApiVersion { get; set; } = null!; - /// - /// Indicates the direction of the call. In most cases this will be inbound, but if you are using Dial it will be outbound-dial - /// - public string Direction { get; set; } - - /// - /// This parameter is set only when Twilio receives a forwarded call, but its value depends on the caller's carrier including information when forwarding. Not all carriers support passing this information - /// - public string ForwardedFrom { get; set; } - - /// - /// This parameter is set when the IncomingPhoneNumber that received the call has had its VoiceCallerIdLookup value set to true. - /// - public string CallerName { get; set; } + /// + /// Indicates the direction of the call. In most cases this will be inbound, but if you are using Dial it will be outbound-dial + /// + public string Direction { get; set; } = null!; + + /// + /// This parameter is set only when Twilio receives a forwarded call, but its value depends on the caller's carrier including information when forwarding. Not all carriers support passing this information + /// + public string? ForwardedFrom { get; set; } + + /// + /// 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 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; } + /// A token string needed to invoke a forwarded call. + public string? CallToken { get; set; } - #region Gather & Record Parameters + #region Gather & Record Parameters - /// - /// When used with the Gather verb, the digits the caller pressed, excluding the finishOnKey digit if used. - /// When used with the Record verb, the key (if any) pressed to end the recording or 'hangup' if the caller hung up - /// - public string Digits { get; set; } + /// + /// When used with the Gather verb, the digits the caller pressed, excluding the finishOnKey digit if used. + /// When used with the Record verb, the key (if any) pressed to end the recording or 'hangup' if the caller hung up + /// + public string? Digits { get; set; } - /// - /// When used with the Gather verb, the transcribed result of the speech - /// - public string SpeechResult { get; set; } + /// + /// When used with the Gather verb, the transcribed result of the speech + /// + public string? SpeechResult { get; set; } - /// - /// When used with the Gather verb, a confidence score between 0.0 and 1.0 respectively. - /// A higher confidence score means a greater likelihood that recognized words are correct. - /// - public float? Confidence { get; set; } + /// + /// When used with the Gather verb, a confidence score between 0.0 and 1.0 respectively. + /// A higher confidence score means a greater likelihood that recognized words are correct. + /// + public float? Confidence { get; set; } - /// - /// The URL of the recorded audio. When the result of a transcription, the URL for the transcription's source recording resource. - /// - public string RecordingUrl { get; set; } - - /// - /// The status of the recording. Possible values are: completed, failed. - /// - public string RecordingStatus { get; set; } - - /// - /// The duration of the recorded audio (in seconds) - /// - public string RecordingDuration { get; set; } - - /// - /// The number of channels in the final recording file as an integer. - /// - public int? RecordingChannels { get; set; } - - /// - /// The source of the recorded audio. - /// - public string RecordingSource { get; set; } - - /// - /// The key used to submit the digits - /// - public string FinishedOnKey { get; set; } + /// + /// The URL of the recorded audio. When the result of a transcription, the URL for the transcription's source recording resource. + /// + public string? RecordingUrl { get; set; } + + /// + /// The status of the recording. Possible values are: completed, failed. + /// + public string? RecordingStatus { get; set; } + + /// + /// The duration of the recorded audio (in seconds) + /// + public string? RecordingDuration { get; set; } + + /// + /// The number of channels in the final recording file as an integer. + /// + public int? RecordingChannels { get; set; } + + /// + /// The source of the recorded audio. + /// + public string? RecordingSource { get; set; } + + /// + /// The key used to submit the digits + /// + public string? FinishedOnKey { get; set; } - #endregion + #endregion - #region Transcription Parameters + #region Transcription Parameters - /// - /// The unique 34 character ID of the transcription - /// - public string TranscriptionSid { get; set; } + /// + /// The unique 34 character ID of the transcription + /// + public string? TranscriptionSid { get; set; } - /// - /// Contains the text of the transcription - /// - public string TranscriptionText { get; set; } + /// + /// Contains the text of the transcription + /// + public string? TranscriptionText { get; set; } - /// - /// The status of the transcription attempt: either 'completed' or 'failed' - /// - public string TranscriptionStatus { get; set; } + /// + /// The status of the transcription attempt: either 'completed' or 'failed' + /// + public string? TranscriptionStatus { get; set; } - /// - /// The URL for the transcription's REST API resource - /// - public string TranscriptionUrl { get; set; } + /// + /// The URL for the transcription's REST API resource + /// + public string? TranscriptionUrl { get; set; } - /// - /// The unique 34 character ID of the recording from which the transcription was generated - /// - public string RecordingSid { get; set; } + /// + /// The unique 34 character ID of the recording from which the transcription was generated + /// + public string? RecordingSid { get; set; } - #endregion + #endregion - #region Dial Parameters + #region Dial Parameters - /// - /// The outcome of the Dial attempt. See the DialCallStatus section below for details - /// - public string DialCallStatus { get; set; } + /// + /// The outcome of the Dial attempt. See the DialCallStatus section below for details + /// + public string? DialCallStatus { get; set; } - /// - /// The call sid of the new call leg. This parameter is not sent after dialing a conference - /// - public string DialCallSid { get; set; } + /// + /// The call sid of the new call leg. This parameter is not sent after dialing a conference + /// + public string? DialCallSid { get; set; } - /// - /// The duration in seconds of the dialed call. This parameter is not sent after dialing a conference - /// - public string DialCallDuration { get; set; } + /// + /// The duration in seconds of the dialed call. This parameter is not sent after dialing a conference + /// + public string? DialCallDuration { get; set; } - #endregion + #endregion - #region SIP Parameters + #region SIP Parameters - /// - /// The Twilio SIP Domain to which the INVITE was sent - /// - public string SipDomain { get; set; } + /// + /// The Twilio SIP Domain to which the INVITE was sent + /// + public string? SipDomain { get; set; } - /// - /// The username given when authenticating the request, if Credential List is the authentication method. - /// - public string SipUsername { get; set; } + /// + /// The username given when authenticating the request, if Credential List is the authentication method. + /// + public string? SipUsername { get; set; } - /// - /// The Call-Id of the incoming INVITE - /// - public string SipCallId { get; set; } + /// + /// The Call-Id of the incoming INVITE + /// + public string? SipCallId { get; set; } - /// - /// The IP Address the incoming INVITE came from. - /// - public string SipSourceIp { get; set; } + /// + /// The IP Address the incoming INVITE came from. + /// + public string? SipSourceIp { get; set; } - #endregion - } -} + #endregion +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs b/src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs index a3bd032..dca22f0 100644 --- a/src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs +++ b/src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs @@ -1,7 +1,4 @@ -using System; using System.Net; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; namespace Twilio.AspNet.Core.UnitTests; @@ -11,12 +8,12 @@ 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, + 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)); @@ -35,29 +32,27 @@ public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null Request.Setup(x => x.Headers).Returns(headers); Request.Setup(x => x.HttpContext).Returns(HttpContext.Object); - var uri = new Uri(ContextMocks.fakeUrl); + var uri = new Uri(FakeUrl); Request.Setup(x => x.QueryString).Returns(new QueryString(uri.Query)); Request.Setup(x => x.Scheme).Returns(uri.Scheme); Request.Setup(x => x.Host).Returns(new HostString(uri.Host)); Request.Setup(x => x.Path).Returns(new PathString(uri.AbsolutePath)); - if (form != null) - { - Request.Setup(x => x.Method).Returns("POST"); - Request.Setup(x => x.Form).Returns(form); - Request.Setup(x => x.ReadFormAsync(new CancellationToken())) - .Returns(() => Task.FromResult((IFormCollection)form)); - Request.Setup(x => x.HasFormContentType).Returns(true); - } + if (form is null) return; + Request.Setup(x => x.Method).Returns("POST"); + Request.Setup(x => x.Form).Returns(form); + Request.Setup(x => x.ReadFormAsync(new CancellationToken())) + .Returns(() => Task.FromResult(form)); + Request.Setup(x => x.HasFormContentType).Returns(true); } - public static string fakeUrl = "https://api.example.com/webhook"; - public static string fakeAuthToken = "thisisafakeauthtoken"; + public const string FakeUrl = "https://api.example.com/webhook"; + public const string FakeAuthToken = "thisisafakeauthtoken"; - private static string CalculateSignature(string urlOverride, FormCollection form) + private static string CalculateSignature(string? urlOverride, FormCollection? form) => ValidationHelper.CalculateSignature( - string.IsNullOrEmpty(urlOverride) ? fakeUrl : urlOverride, - fakeAuthToken, + string.IsNullOrEmpty(urlOverride) ? FakeUrl : urlOverride, + FakeAuthToken, form ); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core.UnitTests/Extensions.cs b/src/Twilio.AspNet.Core.UnitTests/Extensions.cs index 6afe54c..3365a41 100644 --- a/src/Twilio.AspNet.Core.UnitTests/Extensions.cs +++ b/src/Twilio.AspNet.Core.UnitTests/Extensions.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Options; namespace Twilio.AspNet.Core.UnitTests; diff --git a/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs b/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs index 134b55c..3d904e2 100644 --- a/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/MinimalApiTwiMLResultTests.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Twilio.TwiML; using Xunit; diff --git a/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs b/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs index 3ba40c2..bdb5cb3 100644 --- a/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; @@ -30,7 +28,7 @@ public void TestNoLocalDueToProxy() public void TestNoLocal() { var fakeContext = new ContextMocks(true).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", false); + var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token"); Assert.False(result); } @@ -39,7 +37,7 @@ public void TestNoLocal() public void TestNoForm() { var fakeContext = new ContextMocks(true).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false); + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken); Assert.True(result); } @@ -52,7 +50,7 @@ public void TestBadForm() contextMocks.Request.Setup(x => x.Method).Returns("POST"); contextMocks.Request.Setup(x => x.Form).Throws(new Exception("poof!")); - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false); + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken); Assert.True(result); } @@ -62,8 +60,7 @@ public void TestUrlOverrideFail() { var fakeContext = new ContextMocks(true).HttpContext.Object; var result = - RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", - false); + RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken, "https://example.com/"); Assert.False(result); } @@ -73,8 +70,7 @@ public void TestUrlOverride() { var fakeContext = new ContextMocks("https://example.com/", true).HttpContext.Object; var result = - RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", - false); + RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken, "https://example.com/"); Assert.True(result); } @@ -88,7 +84,7 @@ public void TestForm() { "key2", "value2" } }); var fakeContext = new ContextMocks(true, form).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false); + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken); Assert.True(result); } 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 ae6eaf2..5622eeb 100644 --- a/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj +++ b/src/Twilio.AspNet.Core.UnitTests/Twilio.AspNet.Core.UnitTests.csproj @@ -1,15 +1,20 @@ - net7.0 + net9.0 false - disable + enable + enable - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioClientOptionsTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioClientOptionsTests.cs index 34176b1..2f9a898 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioClientOptionsTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioClientOptionsTests.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; -using System.IO; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -35,7 +32,7 @@ public void AddTwilioClient_With_Callback_Should_Match_Configuration() serviceCollection.AddSingleton(BuildEmptyConfiguration()); serviceCollection.AddTwilioClient((_, options) => { - var client = ValidTwilioOptions.Client; + var client = ValidTwilioOptions.Client ?? throw new Exception("Client options not configured."); options.AccountSid = client.AccountSid; options.AuthToken = client.AuthToken; options.ApiKeySid = client.ApiKeySid; @@ -81,11 +78,10 @@ public void AddTwilioClient_Should_Fallback_To_AuthToken() { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken), - new KeyValuePair("Twilio:Client:AccountSid", ValidTwilioOptions.Client.AccountSid) - }) + .AddInMemoryCollection([ + new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken), + new KeyValuePair("Twilio:Client:AccountSid", ValidTwilioOptions.Client.AccountSid) + ]) .Build(); serviceCollection.AddSingleton(configuration); @@ -164,11 +160,10 @@ public void AddTwilioClient_AuthToken_Without_Config_Should_Sanitize_Options() { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio:Client:AccountSid", "AccountSid"), - new KeyValuePair("Twilio:Client:AuthToken", "AuthToken"), - }).Build(); + .AddInMemoryCollection([ + new KeyValuePair("Twilio:Client:AccountSid", "AccountSid"), + new KeyValuePair("Twilio:Client:AuthToken", "AuthToken") + ]).Build(); serviceCollection.AddSingleton(configuration); serviceCollection.AddTwilioClient(); @@ -187,12 +182,11 @@ public void AddTwilioClient_ApiKey_Without_Config_Should_Sanitize_Options() { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio:Client:AccountSid", "AccountSid"), - new KeyValuePair("Twilio:Client:ApiKeySid", "ApiKeySid"), - new KeyValuePair("Twilio:Client:ApiKeySecret", "ApiKeySecret"), - }).Build(); + .AddInMemoryCollection([ + new KeyValuePair("Twilio:Client:AccountSid", "AccountSid"), + new KeyValuePair("Twilio:Client:ApiKeySid", "ApiKeySid"), + new KeyValuePair("Twilio:Client:ApiKeySecret", "ApiKeySecret") + ]).Build(); serviceCollection.AddSingleton(configuration); serviceCollection.AddTwilioClient(); diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs index 151dbf9..d133b73 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioClientTests.cs @@ -1,9 +1,6 @@ -using System; -using System.IO; using System.Reflection; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -66,8 +63,8 @@ public void AddTwilioClient_With_ValidOptions_Should_AddTwilioClient() using var scope = host.Services.CreateScope(); var twilioRestClients = new[] { - scope.ServiceProvider.GetService(), - (TwilioRestClient)scope.ServiceProvider.GetService() + scope.ServiceProvider.GetRequiredService(), + (TwilioRestClient)scope.ServiceProvider.GetRequiredService() }; foreach (var client in twilioRestClients) { @@ -82,15 +79,16 @@ public void AddTwilioClient_With_ApiKeyOptions_Should_Match_Properties() using var scope = host.Services.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); + Assert.NotNull(ApiKeyTwilioOptions.Client); Assert.Equal(ApiKeyTwilioOptions.Client.Region, client.Region); Assert.Equal(ApiKeyTwilioOptions.Client.Edge, client.Edge); Assert.Equal(ApiKeyTwilioOptions.Client.AccountSid, client.AccountSid); Assert.Equal(ApiKeyTwilioOptions.Client.LogLevel, client.LogLevel); Assert.Equal(ApiKeyTwilioOptions.Client.ApiKeySid, - typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); Assert.Equal(ApiKeyTwilioOptions.Client.ApiKeySecret, - typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); } @@ -101,15 +99,16 @@ public void AddTwilioClient_With_AuthTokenOptions_Should_Match_Properties() using var scope = host.Services.CreateScope(); var client = scope.ServiceProvider.GetRequiredService(); + Assert.NotNull(AuthTokenTwilioOptions.Client); Assert.Equal(AuthTokenTwilioOptions.Client.Region, client.Region); Assert.Equal(AuthTokenTwilioOptions.Client.Edge, client.Edge); Assert.Equal(AuthTokenTwilioOptions.Client.AccountSid, client.AccountSid); Assert.Equal(AuthTokenTwilioOptions.Client.LogLevel, client.LogLevel); Assert.Equal(AuthTokenTwilioOptions.Client.AccountSid, - typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); Assert.Equal(AuthTokenTwilioOptions.Client.AuthToken, - typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); } @@ -167,15 +166,16 @@ public async Task AddTwilioClient_Should_Use_Reloaded_Configuration() { var client = scope.ServiceProvider.GetRequiredService(); + Assert.NotNull(ValidTwilioOptions.Client); Assert.Equal(ValidTwilioOptions.Client.Region, client.Region); Assert.Equal(ValidTwilioOptions.Client.Edge, client.Edge); 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)); } @@ -211,10 +211,10 @@ public async Task AddTwilioClient_Should_Use_Reloaded_Configuration() Assert.Equal(updatedOptions.Client.AccountSid, client.AccountSid); Assert.Equal(updatedOptions.Client.LogLevel, client.LogLevel); Assert.Equal(updatedOptions.Client.AccountSid, - typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_username", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); Assert.Equal(updatedOptions.Client.AuthToken, - typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance)! + typeof(TwilioRestClient).GetField("_password", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(client)); } } @@ -228,7 +228,7 @@ public void AddTwilioClient_Should_Add_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); diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs index 78ad6e7..5d0cf0d 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerExtensionTests.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Threading.Tasks; -using System.Xml.Linq; +using System.Xml.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerTests.cs index bacdfee..c9e7418 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioControllerTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioControllerTests.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/src/Twilio.AspNet.Core.UnitTests/TwilioRequestValidationOptionsTests.cs b/src/Twilio.AspNet.Core.UnitTests/TwilioRequestValidationOptionsTests.cs index 0bf565e..a58780a 100644 --- a/src/Twilio.AspNet.Core.UnitTests/TwilioRequestValidationOptionsTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/TwilioRequestValidationOptionsTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; -using System.Threading.Tasks; +using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -50,16 +46,15 @@ public void AddTwilioRequestValidation_From_Configuration_Should_Match_Configura { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken), - new KeyValuePair( + .AddInMemoryCollection([ + new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken), + new KeyValuePair( "Twilio:RequestValidation:AuthToken", ValidTwilioOptions.RequestValidation.AuthToken), - new KeyValuePair( + new KeyValuePair( "Twilio:RequestValidation:BaseUrlOverride", ValidTwilioOptions.RequestValidation.BaseUrlOverride), - new KeyValuePair( + new KeyValuePair( "Twilio:RequestValidation:AllowLocal", ValidTwilioOptions.RequestValidation.AllowLocal.ToString()) - }) + ]) .Build(); serviceCollection.AddSingleton(configuration); @@ -79,15 +74,14 @@ public void AddTwilioRequestValidation_From_ConfigurationSection_Should_Match_Co { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { + .AddInMemoryCollection([ new KeyValuePair( "Twilio:AuthToken", ValidTwilioOptions.RequestValidation.AuthToken), new KeyValuePair( "Twilio:BaseUrlOverride", ValidTwilioOptions.RequestValidation.BaseUrlOverride), new KeyValuePair( "Twilio:AllowLocal", ValidTwilioOptions.RequestValidation.AllowLocal.ToString()) - }) + ]) .Build(); serviceCollection.AddSingleton(configuration); @@ -122,10 +116,9 @@ public void AddTwilioRequestValidation_Should_Fallback_To_AuthToken() { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken) - }) + .AddInMemoryCollection([ + new KeyValuePair("Twilio:AuthToken", ValidTwilioOptions.AuthToken) + ]) .Build(); serviceCollection.AddSingleton(configuration); @@ -158,11 +151,10 @@ public void AddTwilioRequestValidation_Without_AuthToken_Should_Throw() { var serviceCollection = new ServiceCollection(); var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new[] - { - new KeyValuePair("Twilio", null), - new KeyValuePair("Twilio:RequestValidation:AuthToken", null) - }).Build(); + .AddInMemoryCollection([ + new KeyValuePair("Twilio", null), + new KeyValuePair("Twilio:RequestValidation:AuthToken", null) + ]).Build(); serviceCollection.AddSingleton(configuration); serviceCollection.AddTwilioRequestValidation(); diff --git a/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestTests.cs b/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestTests.cs index 35fb00b..45e1540 100644 --- a/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestTests.cs +++ b/src/Twilio.AspNet.Core.UnitTests/ValidateTwilioRequestTests.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; using System.Text.Json; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -53,13 +49,11 @@ public async Task Validate_Request_Successfully(Type type) { "From", "+1234567890" }, { "Body", "Ahoy!" } }); - c.Request.Headers.Add( - "X-Twilio-Signature", ValidationHelper.CalculateSignature( + c.Request.Headers["X-Twilio-Signature"] = ValidationHelper.CalculateSignature( $"{ValidTwilioOptions.RequestValidation.BaseUrlOverride.TrimEnd('/')}{c.Request.Path}", ValidTwilioOptions.RequestValidation.AuthToken, c.Request.Form - ) - ); + ); }); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); @@ -87,7 +81,7 @@ public async Task Validate_Request_Forbid(Type type) { "From", "+1234567890" }, { "Body", "Ahoy!" } }); - c.Request.Headers.Add("X-Twilio-Signature", "sldflsjf"); + c.Request.Headers["X-Twilio-Signature"] = "sldflsjf"; }); Assert.Equal(StatusCodes.Status403Forbidden, context.Response.StatusCode); @@ -121,13 +115,12 @@ public async Task Validate_Request_With_ReloadOnChange(Type type) { "From", "+1234567890" }, { "Body", "Ahoy!" } }); - c.Request.Headers.Add( - "X-Twilio-Signature", ValidationHelper.CalculateSignature( + c.Request.Headers["X-Twilio-Signature"] = ValidationHelper.CalculateSignature( $"{ValidTwilioOptions.RequestValidation.BaseUrlOverride.TrimEnd('/')}{c.Request.Path}", ValidTwilioOptions.RequestValidation.AuthToken, c.Request.Form ) - ); +; }); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); @@ -158,13 +151,12 @@ public async Task Validate_Request_With_ReloadOnChange(Type type) { "From", "+1234567890" }, { "Body", "Ahoy!" } }); - c.Request.Headers.Add( - "X-Twilio-Signature", ValidationHelper.CalculateSignature( + c.Request.Headers["X-Twilio-Signature"] = ValidationHelper.CalculateSignature( $"{updatedOptions.RequestValidation.BaseUrlOverride.TrimEnd('/')}{c.Request.Path}", updatedOptions.RequestValidation.AuthToken, c.Request.Form ) - ); +; }); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); @@ -190,7 +182,7 @@ public async Task Validate_Request_With_ReloadOnChange(Type type) c.Request.Host = new HostString("localhost"); c.Request.Method = HttpMethods.Post; c.Request.Path = "/sms"; - c.Request.Headers.Add("X-Twilio-Signature", "sdfsjf"); + c.Request.Headers["X-Twilio-Signature"] = "sdfsjf"; }); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); diff --git a/src/Twilio.AspNet.Core.UnitTests/ValidationHelper.cs b/src/Twilio.AspNet.Core.UnitTests/ValidationHelper.cs index 757b981..abdb606 100644 --- a/src/Twilio.AspNet.Core.UnitTests/ValidationHelper.cs +++ b/src/Twilio.AspNet.Core.UnitTests/ValidationHelper.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; @@ -8,10 +6,10 @@ namespace Twilio.AspNet.Core.UnitTests; internal static class ValidationHelper { - internal static string CalculateSignature(string url, string authToken, IFormCollection form) + internal static string CalculateSignature(string url, string authToken, IFormCollection? form) { var value = new StringBuilder(url); - if (form != null) + if (form is not null) { var sortedKeys = form.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList(); foreach (var key in sortedKeys) diff --git a/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs b/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs index 6b5e9f7..07d5b18 100644 --- a/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs +++ b/src/Twilio.AspNet.Core/MinimalApiTwiMLResult.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNetCore.Http; diff --git a/src/Twilio.AspNet.Core/RequestValidationDependencyInjectionExtensions.cs b/src/Twilio.AspNet.Core/RequestValidationDependencyInjectionExtensions.cs index fddf5db..54a99a8 100644 --- a/src/Twilio.AspNet.Core/RequestValidationDependencyInjectionExtensions.cs +++ b/src/Twilio.AspNet.Core/RequestValidationDependencyInjectionExtensions.cs @@ -1,103 +1,101 @@ -using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +public static class RequestValidationDependencyInjectionExtensions { - public static class RequestValidationDependencyInjectionExtensions + public static IServiceCollection AddTwilioRequestValidation(this IServiceCollection services) { - public static IServiceCollection AddTwilioRequestValidation(this IServiceCollection services) + var optionsBuilder = services.AddOptions(); + optionsBuilder.Configure((options, config) => { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Configure((options, config) => + var twilioSection = config.GetSection("Twilio"); + if (twilioSection.Exists() == false) { - var twilioSection = config.GetSection("Twilio"); - if (twilioSection.Exists() == false) - { - throw new Exception("Twilio options not configured."); - } + throw new Exception("Twilio options not configured."); + } - var requestValidationSection = config.GetSection("Twilio:RequestValidation"); - requestValidationSection.Bind(options); + var requestValidationSection = config.GetSection("Twilio:RequestValidation"); + requestValidationSection.Bind(options); - var authTokenFallback = twilioSection["AuthToken"]; - if (string.IsNullOrEmpty(options.AuthToken) && !string.IsNullOrEmpty(authTokenFallback)) - options.AuthToken = authTokenFallback; - }); - optionsBuilder.Services.AddSingleton< - IOptionsChangeTokenSource, - ConfigurationChangeTokenSource - >(); - Sanitize(optionsBuilder); - Validate(optionsBuilder); - return services; - } + var authTokenFallback = twilioSection["AuthToken"]; + if (string.IsNullOrEmpty(options.AuthToken) && !string.IsNullOrEmpty(authTokenFallback)) + options.AuthToken = authTokenFallback; + }); + optionsBuilder.Services.AddSingleton< + IOptionsChangeTokenSource, + ConfigurationChangeTokenSource + >(); + Sanitize(optionsBuilder); + Validate(optionsBuilder); + return services; + } - public static IServiceCollection AddTwilioRequestValidation( - this IServiceCollection services, - IConfiguration namedConfigurationSection - ) - { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Bind(namedConfigurationSection); - Validate(optionsBuilder); - Sanitize(optionsBuilder); - return services; - } + public static IServiceCollection AddTwilioRequestValidation( + this IServiceCollection services, + IConfiguration namedConfigurationSection + ) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.Bind(namedConfigurationSection); + Validate(optionsBuilder); + Sanitize(optionsBuilder); + return services; + } - public static IServiceCollection AddTwilioRequestValidation( - this IServiceCollection services, - Action configureOptions - ) - => AddTwilioRequestValidation(services, (_, options) => configureOptions(options)); + public static IServiceCollection AddTwilioRequestValidation( + this IServiceCollection services, + Action configureOptions + ) + => AddTwilioRequestValidation(services, (_, options) => configureOptions(options)); - public static IServiceCollection AddTwilioRequestValidation( - this IServiceCollection services, - Action configureOptions - ) - { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Configure((options, provider) => configureOptions(provider, options)); - Sanitize(optionsBuilder); - Validate(optionsBuilder); - return services; - } + public static IServiceCollection AddTwilioRequestValidation( + this IServiceCollection services, + Action configureOptions + ) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.Configure((options, provider) => configureOptions(provider, options)); + Sanitize(optionsBuilder); + Validate(optionsBuilder); + return services; + } - public static IServiceCollection AddTwilioRequestValidation( - this IServiceCollection services, - TwilioRequestValidationOptions options - ) + public static IServiceCollection AddTwilioRequestValidation( + this IServiceCollection services, + TwilioRequestValidationOptions options + ) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.Configure((optionsToConfigure, _) => { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Configure((optionsToConfigure, _) => - { - optionsToConfigure.AuthToken = options.AuthToken; - optionsToConfigure.AllowLocal = options.AllowLocal; - optionsToConfigure.BaseUrlOverride = options.BaseUrlOverride; - }); - Sanitize(optionsBuilder); - Validate(optionsBuilder); - return services; - } + optionsToConfigure.AuthToken = options.AuthToken; + optionsToConfigure.AllowLocal = options.AllowLocal; + optionsToConfigure.BaseUrlOverride = options.BaseUrlOverride; + }); + Sanitize(optionsBuilder); + Validate(optionsBuilder); + return services; + } - private static void Sanitize(OptionsBuilder optionsBuilder) + private static void Sanitize(OptionsBuilder optionsBuilder) + { + optionsBuilder.PostConfigure(options => { - optionsBuilder.PostConfigure(options => - { - if (options.AuthToken == "") options.AuthToken = null; - if (options.BaseUrlOverride == "") options.BaseUrlOverride = null; - if (options.BaseUrlOverride != null) options.BaseUrlOverride = options.BaseUrlOverride.TrimEnd('/'); - }); - } + if (options.AuthToken == "") options.AuthToken = null!; + if (options.BaseUrlOverride == "") options.BaseUrlOverride = null; + if (options.BaseUrlOverride is not null) options.BaseUrlOverride = options.BaseUrlOverride.TrimEnd('/'); + }); + } - private static void Validate(OptionsBuilder optionsBuilder) - { - optionsBuilder.Validate( - options => string.IsNullOrEmpty(options.AuthToken) == false, - "Twilio:AuthToken or Twilio:RequestValidation:AuthToken option is required." - ); - } + private static void Validate(OptionsBuilder optionsBuilder) + { + optionsBuilder.Validate( + options => string.IsNullOrEmpty(options.AuthToken) == false, + "Twilio:AuthToken or Twilio:RequestValidation:AuthToken option is required." + ); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/RequestValidationHelper.cs b/src/Twilio.AspNet.Core/RequestValidationHelper.cs index 6323a24..2709bf5 100644 --- a/src/Twilio.AspNet.Core/RequestValidationHelper.cs +++ b/src/Twilio.AspNet.Core/RequestValidationHelper.cs @@ -1,171 +1,166 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; +using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Twilio.Security; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// Class used to validate incoming requests from Twilio using 'Request Validation' as described +/// in the Security section of the Twilio TwiML API documentation. +/// +public static class RequestValidationHelper { /// - /// Class used to validate incoming requests from Twilio using 'Request Validation' as described - /// in the Security section of the Twilio TwiML API documentation. + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute /// - public static class RequestValidationHelper + /// HttpContext to use for validation + internal static async Task IsValidRequestAsync(HttpContext context) { - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - internal static async Task IsValidRequestAsync(HttpContext context) - { - var options = context.RequestServices - .GetRequiredService>().Value; - - var authToken = options.AuthToken; - var baseUrlOverride = options.BaseUrlOverride; - var allowLocal = options.AllowLocal; + var options = context.RequestServices + .GetRequiredService>().Value; - var request = context.Request; + var authToken = options.AuthToken; + var baseUrlOverride = options.BaseUrlOverride; + var allowLocal = options.AllowLocal ?? false; - string urlOverride = null; - if (!string.IsNullOrEmpty(baseUrlOverride)) - { - urlOverride = $"{baseUrlOverride}{request.Path}{request.QueryString}"; - } + var request = context.Request; - return await IsValidRequestAsync(context, authToken, urlOverride, allowLocal).ConfigureAwait(false); + string? urlOverride = null; + if (!string.IsNullOrEmpty(baseUrlOverride)) + { + urlOverride = $"{baseUrlOverride}{request.Path}{request.QueryString}"; } - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// AuthToken for the account used to sign the request - /// - /// Skip validation for local requests. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static Task IsValidRequestAsync(HttpContext context, string authToken, bool allowLocal = false) - => IsValidRequestAsync(context, authToken, null, allowLocal); - - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// 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. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static async Task IsValidRequestAsync( - HttpContext context, - string authToken, - string urlOverride, - bool allowLocal = false - ) - { - if (context.Request.HasFormContentType) - { - // this will load the form async, but then cache is in context.Request.Form which is used later - await context.Request.ReadFormAsync(context.RequestAborted).ConfigureAwait(false); - } + return await IsValidRequestAsync(context, authToken, urlOverride, allowLocal).ConfigureAwait(false); + } + + /// + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute + /// + /// HttpContext to use for validation + /// AuthToken for the account used to sign the request + /// + /// Skip validation for local requests. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static Task IsValidRequestAsync(HttpContext context, string authToken, bool allowLocal = false) + => IsValidRequestAsync(context, authToken, null, allowLocal); - return IsValidRequest(context, authToken, urlOverride, allowLocal); + /// + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute + /// + /// HttpContext to use for validation + /// 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. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static async Task IsValidRequestAsync( + HttpContext context, + string authToken, + string? urlOverride, + bool allowLocal = false + ) + { + if (context.Request.HasFormContentType) + { + // this will load the form async, but then cache is in context.Request.Form which is used later + await context.Request.ReadFormAsync(context.RequestAborted).ConfigureAwait(false); } + + return IsValidRequest(context, authToken, urlOverride, allowLocal); + } - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// AuthToken for the account used to sign the request - /// - /// Skip validation for local requests. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static bool IsValidRequest(HttpContext context, string authToken, bool allowLocal = false) - => IsValidRequest(context, authToken, null, allowLocal); - - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// 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. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static bool IsValidRequest( - HttpContext context, - string authToken, - string urlOverride, - bool allowLocal = false - ) - { - var request = context.Request; + /// + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute + /// + /// HttpContext to use for validation + /// AuthToken for the account used to sign the request + /// + /// Skip validation for local requests. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static bool IsValidRequest(HttpContext context, string authToken, bool allowLocal = false) + => IsValidRequest(context, authToken, null, allowLocal); - if (allowLocal && IsLocal(request)) - { - return true; - } + /// + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute + /// + /// HttpContext to use for validation + /// 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. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static bool IsValidRequest( + HttpContext context, + string authToken, + string? urlOverride, + bool allowLocal = false + ) + { + var request = context.Request; - // validate request - // http://www.twilio.com/docs/security-reliability/security - // Take the full URL of the request, from the protocol (http...) through the end of the query string (everything after the ?) - string fullUrl = string.IsNullOrEmpty(urlOverride) - ? $"{request.Scheme}://{(request.IsHttps ? request.Host.Host : request.Host.ToUriComponent())}{request.Path}{request.QueryString}" - : urlOverride; + if (allowLocal && IsLocal(request)) + { + return true; + } - Dictionary parameters = null; - if (request.HasFormContentType) - { - parameters = request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); - } + // validate request + // http://www.twilio.com/docs/security-reliability/security + // Take the full URL of the request, from the protocol (http...) through the end of the query string (everything after the ?) + var fullUrl = string.IsNullOrEmpty(urlOverride) + ? $"{request.Scheme}://{(request.IsHttps ? request.Host.Host : request.Host.ToUriComponent())}{request.Path}{request.QueryString}" + : urlOverride; - var validator = new RequestValidator(authToken); - return validator.Validate( - url: fullUrl, - parameters: parameters, - expected: request.Headers["X-Twilio-Signature"] - ); + Dictionary? parameters = null; + if (request.HasFormContentType) + { + parameters = request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); } - private static bool IsLocal(HttpRequest req) + var validator = new RequestValidator(authToken); + return validator.Validate( + url: fullUrl, + parameters: parameters, + expected: request.Headers["X-Twilio-Signature"] + ); + } + + private static bool IsLocal(HttpRequest req) + { + if (req.Headers.ContainsKey("X-Forwarded-For")) { - if (req.Headers.ContainsKey("X-Forwarded-For")) - { - // Assume not local if we're behind a proxy - return false; - } + // Assume not local if we're behind a proxy + return false; + } - var connection = req.HttpContext.Connection; - if (connection.RemoteIpAddress != null) + var connection = req.HttpContext.Connection; + if (connection.RemoteIpAddress is not null) + { + if (connection.LocalIpAddress is not null) { - if (connection.LocalIpAddress != null) - { - return connection.RemoteIpAddress.Equals(connection.LocalIpAddress); - } - - return IPAddress.IsLoopback(connection.RemoteIpAddress); + return connection.RemoteIpAddress.Equals(connection.LocalIpAddress); } - // for in memory TestServer or when dealing with default connection info - if (connection.RemoteIpAddress == null && connection.LocalIpAddress == null) - { - return true; - } + return IPAddress.IsLoopback(connection.RemoteIpAddress); + } - return false; + // for in memory TestServer or when dealing with default connection info + if (connection.RemoteIpAddress is null && connection.LocalIpAddress is 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 index cf30494..93640e9 100644 --- a/src/Twilio.AspNet.Core/TwiMLExtensions.cs +++ b/src/Twilio.AspNet.Core/TwiMLExtensions.cs @@ -1,42 +1,41 @@ using System.Xml.Linq; using Twilio.TwiML; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +public static class TwiMLExtensions { - 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 VoiceResponse voiceResponse) + => new(voiceResponse); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// Specifies how to format TwiML - /// - public static TwiMLResult ToTwiMLResult(this VoiceResponse voiceResponse, SaveOptions formattingOptions) - => new TwiMLResult(voiceResponse, formattingOptions); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// Specifies how to format TwiML + /// + public static TwiMLResult ToTwiMLResult(this VoiceResponse voiceResponse, SaveOptions formattingOptions) + => new(voiceResponse, formattingOptions); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - public static TwiMLResult ToTwiMLResult(this MessagingResponse messagingResponse) - => new TwiMLResult(messagingResponse); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + public static TwiMLResult ToTwiMLResult(this MessagingResponse messagingResponse) + => new(messagingResponse); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// Specifies how to format TwiML - /// - public static TwiMLResult ToTwiMLResult(this MessagingResponse messagingResponse, SaveOptions formattingOptions) - => new TwiMLResult(messagingResponse, formattingOptions); - } + /// + /// Returns a properly formatted TwiML response + /// + /// + /// Specifies how to format TwiML + /// + public static TwiMLResult ToTwiMLResult(this MessagingResponse messagingResponse, SaveOptions formattingOptions) + => new(messagingResponse, formattingOptions); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwiMLResult.cs b/src/Twilio.AspNet.Core/TwiMLResult.cs index 9f6f648..e88ea39 100644 --- a/src/Twilio.AspNet.Core/TwiMLResult.cs +++ b/src/Twilio.AspNet.Core/TwiMLResult.cs @@ -1,48 +1,46 @@ -using System.Threading.Tasks; -using System.Xml.Linq; +using System.Xml.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// TwiMLResult writes TwiML to the HTTP response body +/// +public partial class TwiMLResult : IActionResult { - /// - /// TwiMLResult writes TwiML to the HTTP response body - /// - public partial class TwiMLResult : IActionResult - { - private readonly TwiML.TwiML twiml; - private readonly SaveOptions formattingOptions; + private readonly TwiML.TwiML _twiml; + private readonly SaveOptions _formattingOptions; - /// The TwiML to respond with - public TwiMLResult(TwiML.TwiML twiml) : this(twiml, SaveOptions.None) - { - } + /// The TwiML to respond with + public TwiMLResult(TwiML.TwiML twiml) : this(twiml, SaveOptions.None) + { + } - /// The TwiML to respond with - /// Specifies how to format TwiML - public TwiMLResult(TwiML.TwiML twiml, SaveOptions formattingOptions) - { - this.twiml = twiml; - this.formattingOptions = formattingOptions; - } + /// The TwiML to respond with + /// Specifies how to format TwiML + public TwiMLResult(TwiML.TwiML twiml, SaveOptions formattingOptions) + { + _twiml = twiml; + _formattingOptions = formattingOptions; + } - public async Task ExecuteResultAsync(ActionContext actionContext) - { - var response = actionContext.HttpContext.Response; - await WriteTwiMLToResponse(response); - } + public async Task ExecuteResultAsync(ActionContext actionContext) + { + var response = actionContext.HttpContext.Response; + await WriteTwiMLToResponse(response); + } - private async Task WriteTwiMLToResponse(HttpResponse response) + private async Task WriteTwiMLToResponse(HttpResponse response) + { + response.ContentType = "application/xml"; + if (_twiml is null) { - response.ContentType = "application/xml"; - if (twiml == null) - { - await response.WriteAsync(""); - return; - } - - var data = twiml.ToString(formattingOptions); - await response.WriteAsync(data); + await response.WriteAsync(""); + return; } + + var data = _twiml.ToString(_formattingOptions); + await response.WriteAsync(data); } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/Twilio.AspNet.Core.csproj b/src/Twilio.AspNet.Core/Twilio.AspNet.Core.csproj index 426753d..7d56dae 100644 --- a/src/Twilio.AspNet.Core/Twilio.AspNet.Core.csproj +++ b/src/Twilio.AspNet.Core/Twilio.AspNet.Core.csproj @@ -1,19 +1,24 @@  - net6.0;net7.0 + net7.0;net8.0;net9.0 + 13 + enable + enable Library 0.0.0-alpha Twilio.AspNet.Core 0.0.0-alpha Twilio Labs + Twilio helper library for ASP.NET Core Twilio helper library for ASP.NET Core false Refer to the changelog at https://github.com/twilio-labs/twilio-aspnet/blob/main/CHANGELOG.md - Copyright 2022 (c) Twilio, Inc. All rights reserved. + Copyright 2024 (c) Twilio, Inc. All rights reserved. twilio;twiml;sms;voice;telephony;phone;aspnet Apache-2.0 https://github.com/twilio-labs/twilio-aspnet https://github.com/twilio-labs/twilio-aspnet.git + git https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/twilio-icon-64x64.png icon.png README.md @@ -21,31 +26,31 @@ true true snupkg - $([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net7.0')) true - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + <_Parameter1>Twilio.AspNet.Core.UnitTests - - - \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs b/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs index eeaff6e..0f64aeb 100644 --- a/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs +++ b/src/Twilio.AspNet.Core/TwilioClientDependencyInjectionExtensions.cs @@ -1,225 +1,216 @@ -using System; -using System.Net.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Twilio.Clients; using Twilio.Http; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +public static class TwilioClientDependencyInjectionExtensions { - public static class TwilioClientDependencyInjectionExtensions + internal const string TwilioHttpClientName = "Twilio"; + + public static IServiceCollection AddTwilioClient(this IServiceCollection services) { - internal const string TwilioHttpClientName = "Twilio"; + var optionsBuilder = services.AddOptions(); + ConfigureDefaultOptions(optionsBuilder); + PostConfigure(optionsBuilder); + Validate(optionsBuilder); + AddServices(services); + return services; + } - public static IServiceCollection AddTwilioClient(this IServiceCollection services) - { - var optionsBuilder = services.AddOptions(); - ConfigureDefaultOptions(optionsBuilder); - PostConfigure(optionsBuilder); - Validate(optionsBuilder); - AddServices(services); - return services; - } + public static IServiceCollection AddTwilioClient( + this IServiceCollection services, + IConfiguration namedConfigurationSection + ) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.Bind(namedConfigurationSection); + PostConfigure(optionsBuilder); + Validate(optionsBuilder); + AddServices(services); + return services; + } - public static IServiceCollection AddTwilioClient( - this IServiceCollection services, - IConfiguration namedConfigurationSection - ) - { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Bind(namedConfigurationSection); - PostConfigure(optionsBuilder); - Validate(optionsBuilder); - AddServices(services); - return services; - } + public static IServiceCollection AddTwilioClient( + this IServiceCollection services, + Action configureOptions + ) + => AddTwilioClient(services, (_, options) => configureOptions(options)); - public static IServiceCollection AddTwilioClient( - this IServiceCollection services, - Action configureOptions - ) - => AddTwilioClient(services, (_, options) => configureOptions(options)); + public static IServiceCollection AddTwilioClient( + this IServiceCollection services, + Action configureOptions + ) + { + var optionsBuilder = services.AddOptions(); + optionsBuilder.Configure((options, provider) => configureOptions(provider, options)); + PostConfigure(optionsBuilder); + Validate(optionsBuilder); + AddServices(services); + return services; + } - public static IServiceCollection AddTwilioClient( - this IServiceCollection services, - Action configureOptions - ) - { - var optionsBuilder = services.AddOptions(); - optionsBuilder.Configure((options, provider) => configureOptions(provider, options)); - PostConfigure(optionsBuilder); - Validate(optionsBuilder); - AddServices(services); - return services; - } + public static IServiceCollection AddTwilioClient( + this IServiceCollection services, + TwilioClientOptions options + ) + { + var optionsBuilder = services.AddOptions(); - public static IServiceCollection AddTwilioClient( - this IServiceCollection services, - TwilioClientOptions options - ) + optionsBuilder.Configure((optionsToConfigure, _) => { - var optionsBuilder = services.AddOptions(); + optionsToConfigure.AccountSid = options.AccountSid; + optionsToConfigure.AuthToken = options.AuthToken; + optionsToConfigure.ApiKeySid = options.ApiKeySid; + optionsToConfigure.ApiKeySecret = options.ApiKeySecret; + optionsToConfigure.CredentialType = options.CredentialType; + optionsToConfigure.Edge = options.Edge; + optionsToConfigure.Region = options.Region; + optionsToConfigure.LogLevel = options.LogLevel; + }); + PostConfigure(optionsBuilder); + Validate(optionsBuilder); + AddServices(services); + return services; + } - optionsBuilder.Configure((optionsToConfigure, _) => + private static void ConfigureDefaultOptions(OptionsBuilder optionsBuilder) + { + optionsBuilder.Configure((options, config) => + { + var twilioSection = config.GetSection("Twilio"); + if (twilioSection.Exists() == false) { - optionsToConfigure.AccountSid = options.AccountSid; - optionsToConfigure.AuthToken = options.AuthToken; - optionsToConfigure.ApiKeySid = options.ApiKeySid; - optionsToConfigure.ApiKeySecret = options.ApiKeySecret; - optionsToConfigure.CredentialType = options.CredentialType; - optionsToConfigure.Edge = options.Edge; - optionsToConfigure.Region = options.Region; - optionsToConfigure.LogLevel = options.LogLevel; - }); - PostConfigure(optionsBuilder); - Validate(optionsBuilder); - AddServices(services); - return services; - } + throw new Exception("Twilio options not configured."); + } - private static void ConfigureDefaultOptions(OptionsBuilder optionsBuilder) - { - optionsBuilder.Configure((options, config) => + var clientSection = config.GetSection("Twilio:Client"); + if (clientSection.Exists() == false) { - var twilioSection = config.GetSection("Twilio"); - if (twilioSection.Exists() == false) - { - throw new Exception("Twilio options not configured."); - } - - var clientSection = config.GetSection("Twilio:Client"); - if (clientSection.Exists() == false) - { - throw new Exception("Twilio:Client options not configured."); - } + throw new Exception("Twilio:Client options not configured."); + } - clientSection.Bind(options); + clientSection.Bind(options); - var authTokenFallback = twilioSection["AuthToken"]; - if (string.IsNullOrEmpty(options.AuthToken) && !string.IsNullOrEmpty(authTokenFallback)) - options.AuthToken = authTokenFallback; - }); - optionsBuilder.Services.AddSingleton< - IOptionsChangeTokenSource, - ConfigurationChangeTokenSource - >(); - } + var authTokenFallback = twilioSection["AuthToken"]; + if (string.IsNullOrEmpty(options.AuthToken) && !string.IsNullOrEmpty(authTokenFallback)) + options.AuthToken = authTokenFallback; + }); + optionsBuilder.Services.AddSingleton< + IOptionsChangeTokenSource, + ConfigurationChangeTokenSource + >(); + } - private static void PostConfigure(OptionsBuilder optionsBuilder) - => optionsBuilder - .PostConfigure(Sanitize) - .PostConfigure(ConfigureCredentialType); + private static void PostConfigure(OptionsBuilder optionsBuilder) + => optionsBuilder + .PostConfigure(Sanitize) + .PostConfigure(ConfigureCredentialType); - private static void AddServices(IServiceCollection services) - { - services.AddHttpClient(TwilioHttpClientName) - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - // same options as the Twilio C# SDK - AllowAutoRedirect = false - }); + private static void AddServices(IServiceCollection services) + { + services.AddHttpClient(TwilioHttpClientName) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + // same options as the Twilio C# SDK + AllowAutoRedirect = false + }); - services.AddScoped(CreateTwilioClient); - services.AddScoped(CreateTwilioClient); - } + services.AddScoped(CreateTwilioClient); + services.AddScoped(CreateTwilioClient); + } - private static void Sanitize(TwilioClientOptions options) - { - if (options.AccountSid == "") options.AccountSid = null; - if (options.AuthToken == "") options.AuthToken = null; - if (options.ApiKeySid == "") options.ApiKeySid = null; - if (options.ApiKeySecret == "") options.ApiKeySecret = null; - if (options.Region == "") options.Region = null; - if (options.Edge == "") options.Edge = null; - if (options.LogLevel == "") options.LogLevel = null; - } - - private static void Validate(OptionsBuilder optionsBuilder) - { - optionsBuilder.Validate( - options => options.CredentialType != CredentialType.Unspecified, - "Twilio:Client:CredentialType could not be determined. Configure as ApiKey or AuthToken." - ); - optionsBuilder.Validate(options => - { - var isApiKeyConfigured = options.AccountSid != null && - options.ApiKeySid != null && - options.ApiKeySecret != null; - return options.CredentialType != CredentialType.ApiKey || isApiKeyConfigured; - }, "Twilio:Client:{AccountSid|ApiKeySid|ApiKeySecret} options required for CredentialType.ApiKey." - ); - optionsBuilder.Validate(options => + private static void Sanitize(TwilioClientOptions options) + { + if (options.AccountSid == "") options.AccountSid = null!; + if (options.AuthToken == "") options.AuthToken = null; + if (options.ApiKeySid == "") options.ApiKeySid = null; + if (options.ApiKeySecret == "") options.ApiKeySecret = null; + if (options.Region == "") options.Region = null; + if (options.Edge == "") options.Edge = null; + if (options.LogLevel == "") options.LogLevel = null; + } + + private static void Validate(OptionsBuilder optionsBuilder) + { + optionsBuilder.Validate( + options => options.CredentialType != CredentialType.Unspecified, + "Twilio:Client:CredentialType could not be determined. Configure as ApiKey or AuthToken." + ); + optionsBuilder.Validate(options => + { + var isApiKeyConfigured = options is { AccountSid: not null, ApiKeySid: not null, ApiKeySecret: not null }; + return options.CredentialType != CredentialType.ApiKey || isApiKeyConfigured; + }, "Twilio:Client:{AccountSid|ApiKeySid|ApiKeySecret} options required for CredentialType.ApiKey." + ); + optionsBuilder.Validate(options => + { + var isAuthTokenConfigured = options is { AccountSid: not null, AuthToken: not null }; + if (options.CredentialType == CredentialType.AuthToken && !isAuthTokenConfigured) { - var isAuthTokenConfigured = options.AccountSid != null && - options.AuthToken != null; - if (options.CredentialType == CredentialType.AuthToken && !isAuthTokenConfigured) - { - return false; - } - - return true; - }, "Twilio:Client:{AccountSid|AuthToken} options required for CredentialType.AuthToken." - ); - } + return false; + } - private static void ConfigureCredentialType(TwilioClientOptions options) - { - if (options.CredentialType != CredentialType.Unspecified) return; + return true; + }, "Twilio:Client:{AccountSid|AuthToken} options required for CredentialType.AuthToken." + ); + } - var isApiKeyConfigured = options.AccountSid != null && - options.ApiKeySid != null && - options.ApiKeySecret != null; - var isAuthTokenConfigured = options.AccountSid != null && - options.AuthToken != null; + private static void ConfigureCredentialType(TwilioClientOptions options) + { + if (options.CredentialType != CredentialType.Unspecified) return; - if (isApiKeyConfigured) options.CredentialType = CredentialType.ApiKey; - else if (isAuthTokenConfigured) options.CredentialType = CredentialType.AuthToken; - } + var isApiKeyConfigured = options is { AccountSid: not null, ApiKeySid: not null, ApiKeySecret: not null }; + var isAuthTokenConfigured = options is { AccountSid: not null, AuthToken: not null }; - private static TwilioRestClient CreateTwilioClient(IServiceProvider provider) - { - var httpClient = provider.GetRequiredService().CreateClient(TwilioHttpClientName); - Twilio.Http.HttpClient twilioHttpClient = new SystemNetHttpClient(httpClient); + if (isApiKeyConfigured) options.CredentialType = CredentialType.ApiKey; + else if (isAuthTokenConfigured) options.CredentialType = CredentialType.AuthToken; + } - var options = provider.GetRequiredService>().Value; + private static TwilioRestClient CreateTwilioClient(IServiceProvider provider) + { + var httpClient = provider.GetRequiredService().CreateClient(TwilioHttpClientName); + Http.HttpClient twilioHttpClient = new SystemNetHttpClient(httpClient); - TwilioRestClient client; - switch (options.CredentialType) - { - case CredentialType.ApiKey: - client = new TwilioRestClient( - username: options.ApiKeySid, - password: options.ApiKeySecret, - accountSid: options.AccountSid, - region: options.Region, - httpClient: twilioHttpClient, - edge: options.Edge - ); - break; - case CredentialType.AuthToken: - client = new TwilioRestClient( - username: options.AccountSid, - password: options.AuthToken, - accountSid: options.AccountSid, - region: options.Region, - httpClient: twilioHttpClient, - edge: options.Edge - ); - break; - case CredentialType.Unspecified: - default: - throw new Exception("This code should be unreachable"); - } + var options = provider.GetRequiredService>().Value; - if (options.LogLevel != null) - { - client.LogLevel = options.LogLevel; - } + TwilioRestClient client; + switch (options.CredentialType) + { + case CredentialType.ApiKey: + client = new TwilioRestClient( + username: options.ApiKeySid, + password: options.ApiKeySecret, + accountSid: options.AccountSid, + region: options.Region, + httpClient: twilioHttpClient, + edge: options.Edge + ); + break; + case CredentialType.AuthToken: + client = new TwilioRestClient( + username: options.AccountSid, + password: options.AuthToken, + accountSid: options.AccountSid, + region: options.Region, + httpClient: twilioHttpClient, + edge: options.Edge + ); + break; + case CredentialType.Unspecified: + default: + throw new Exception("This code should be unreachable"); + } - return client; + if (options.LogLevel is not null) + { + client.LogLevel = options.LogLevel; } + + return client; } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwilioController.cs b/src/Twilio.AspNet.Core/TwilioController.cs index f6baea7..029bb23 100644 --- a/src/Twilio.AspNet.Core/TwilioController.cs +++ b/src/Twilio.AspNet.Core/TwilioController.cs @@ -2,47 +2,46 @@ using Microsoft.AspNetCore.Mvc; using Twilio.TwiML; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// Extends the standard base controller to simplify returning a TwiML response +/// +public class TwilioController : ControllerBase { /// - /// Extends the standard base controller to simplify returning a TwiML response + /// Returns a properly formatted TwiML response /// - public class TwilioController : ControllerBase - { - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - [NonAction] - public TwiMLResult TwiML(MessagingResponse response) => new TwiMLResult(response); + /// + /// + [NonAction] + public TwiMLResult TwiML(MessagingResponse response) => new(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(MessagingResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - [NonAction] - public TwiMLResult TwiML(VoiceResponse response) => new TwiMLResult(response); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + [NonAction] + public TwiMLResult TwiML(VoiceResponse response) => new(response); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - [NonAction] - public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) - => new TwiMLResult(response, formattingOptions); - } + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + [NonAction] + public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) + => new(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 894e955..d0fae14 100644 --- a/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs +++ b/src/Twilio.AspNet.Core/TwilioControllerExtensions.cs @@ -2,49 +2,48 @@ using Microsoft.AspNetCore.Mvc; using Twilio.TwiML; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// Adds extension methods to the ControllerBase class for returning TwiML in MVC actions +/// +public static class TwilioControllerExtensions { /// - /// Adds extension methods to the ControllerBase class for returning TwiML in MVC actions + /// Returns a properly formatted TwiML response /// - public static class TwilioControllerExtensions - { - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) - => new TwiMLResult(response); + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) + => new(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 + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - 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) + => new(response); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - /// - public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) - => new TwiMLResult(response, formattingOptions); - } + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/TwilioOptions.cs b/src/Twilio.AspNet.Core/TwilioOptions.cs index e15b00f..fd8057d 100644 --- a/src/Twilio.AspNet.Core/TwilioOptions.cs +++ b/src/Twilio.AspNet.Core/TwilioOptions.cs @@ -1,35 +1,34 @@ -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +public class TwilioOptions { - public class TwilioOptions - { - public string AuthToken { get; set; } - public TwilioClientOptions Client { get; set; } - public TwilioRequestValidationOptions RequestValidation { get; set; } - } + public string? AuthToken { get; set; } + public TwilioClientOptions? Client { get; set; } + public TwilioRequestValidationOptions? RequestValidation { get; set; } +} - public class TwilioClientOptions - { - public string AccountSid { get; set; } - public string AuthToken { get; set; } - public string ApiKeySid { get; set; } - public string ApiKeySecret { get; set; } - public CredentialType CredentialType { get; set; } - public string Region { get; set; } - public string Edge { get; set; } - public string LogLevel { get; set; } - } +public class TwilioClientOptions +{ + public string AccountSid { get; set; } = null!; + public string? AuthToken { get; set; } + public string? ApiKeySid { get; set; } + public string? ApiKeySecret { get; set; } + public CredentialType CredentialType { get; set; } + public string? Region { get; set; } + public string? Edge { get; set; } + public string? LogLevel { get; set; } +} - public class TwilioRequestValidationOptions - { - public string AuthToken { get; set; } - public bool AllowLocal { get; set; } - public string BaseUrlOverride { get; set; } - } +public class TwilioRequestValidationOptions +{ + public string AuthToken { get; set; } = null!; + public bool? AllowLocal { get; set; } + public string? BaseUrlOverride { get; set; } +} - public enum CredentialType - { - Unspecified, - AuthToken, - ApiKey - } +public enum CredentialType +{ + Unspecified, + AuthToken, + ApiKey } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs b/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs index 5174fbf..aad0836 100644 --- a/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs +++ b/src/Twilio.AspNet.Core/ValidateRequestAttribute.cs @@ -1,30 +1,27 @@ -using System; -using System.Net; -using System.Threading.Tasks; +using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// Represents an attribute that is used to prevent forgery of a request. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class ValidateRequestAttribute : Attribute, IAsyncActionFilter { - /// - /// Represents an attribute that is used to prevent forgery of a request. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class ValidateRequestAttribute : Attribute, IAsyncActionFilter + public async Task OnActionExecutionAsync( + ActionExecutingContext filterContext, + ActionExecutionDelegate next + ) { - public async Task OnActionExecutionAsync( - ActionExecutingContext filterContext, - ActionExecutionDelegate next - ) + var context = filterContext.HttpContext; + if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false)) { - var context = filterContext.HttpContext; - if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false)) - { - await next(); - return; - } - - filterContext.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden); + await next(); + return; } + + filterContext.Result = new StatusCodeResult((int)HttpStatusCode.Forbidden); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Core/ValidateTwilioRequestFilter.cs b/src/Twilio.AspNet.Core/ValidateTwilioRequestFilter.cs index 9659b00..b4be3a4 100644 --- a/src/Twilio.AspNet.Core/ValidateTwilioRequestFilter.cs +++ b/src/Twilio.AspNet.Core/ValidateTwilioRequestFilter.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -11,7 +10,7 @@ namespace Twilio.AspNet.Core; /// public class ValidateTwilioRequestFilter : IEndpointFilter { - public async ValueTask InvokeAsync( + public async ValueTask InvokeAsync( EndpointFilterInvocationContext efiContext, EndpointFilterDelegate next ) diff --git a/src/Twilio.AspNet.Core/ValidateTwilioRequestMiddleware.cs b/src/Twilio.AspNet.Core/ValidateTwilioRequestMiddleware.cs index 2417856..9cf7df5 100644 --- a/src/Twilio.AspNet.Core/ValidateTwilioRequestMiddleware.cs +++ b/src/Twilio.AspNet.Core/ValidateTwilioRequestMiddleware.cs @@ -1,42 +1,40 @@ using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -namespace Twilio.AspNet.Core +namespace Twilio.AspNet.Core; + +/// +/// Validates that incoming HTTP request originates from Twilio. +/// +public class ValidateTwilioRequestMiddleware { - /// - /// Validates that incoming HTTP request originates from Twilio. - /// - public class ValidateTwilioRequestMiddleware - { - private readonly RequestDelegate next; + private readonly RequestDelegate _next; - public ValidateTwilioRequestMiddleware(RequestDelegate next) - { - this.next = next; - } + public ValidateTwilioRequestMiddleware(RequestDelegate next) + { + _next = next; + } - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context) + { + if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false)) { - if (await RequestValidationHelper.IsValidRequestAsync(context).ConfigureAwait(false)) - { - await next(context); - return; - } - - context.Response.StatusCode = (int)HttpStatusCode.Forbidden; + await _next(context); + return; } + + context.Response.StatusCode = (int)HttpStatusCode.Forbidden; } +} - public static class ValidateTwilioRequestMiddlewareExtensions - { - /// - /// Validates that incoming HTTP request originates from Twilio. - /// - /// - /// - public static IApplicationBuilder UseTwilioRequestValidation(this IApplicationBuilder builder) - => builder.UseMiddleware(); - } +public static class ValidateTwilioRequestMiddlewareExtensions +{ + /// + /// Validates that incoming HTTP request originates from Twilio. + /// + /// + /// + public static IApplicationBuilder UseTwilioRequestValidation(this IApplicationBuilder builder) + => builder.UseMiddleware(); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs b/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs index 5a8bb9f..8b7866e 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs +++ b/src/Twilio.AspNet.Mvc.UnitTests/ContextMocks.cs @@ -1,73 +1,70 @@ -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 +namespace Twilio.AspNet.Mvc.UnitTests; + +public class ContextMocks { - 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 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(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)); + 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 = 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); + 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()); + Request.Setup(x => x.IsLocal).Returns(isLocal); + Request.Setup(x => x.Headers).Returns(headers); + Request.Setup(x => x.Url).Returns(new Uri(FakeUrl)); + if (form is not 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"; + public const string FakeUrl = "https://api.example.com/webhook"; + public const string FakeAuthToken = "thisisafakeauthtoken"; - private string CalculateSignature(string urlOverride, NameValueCollection form) - { - var value = new StringBuilder(); - value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride); + private string CalculateSignature(string? urlOverride, NameValueCollection? form) + { + var value = new StringBuilder(); + value.Append(string.IsNullOrEmpty(urlOverride) ? FakeUrl : urlOverride); - if (form != null) + if (form is not null) + { + var sortedKeys = form.AllKeys.OrderBy(k => k, StringComparer.Ordinal).ToList(); + foreach (var key in sortedKeys) { - var sortedKeys = form.AllKeys.OrderBy(k => k, StringComparer.Ordinal).ToList(); - foreach (var key in sortedKeys) - { - value.Append(key); - value.Append(form[key]); - } + 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())); + var sha1 = new HMACSHA1(Encoding.UTF8.GetBytes(FakeAuthToken)); + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(value.ToString())); - return Convert.ToBase64String(hash); - } + 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 8610945..1f6b9f4 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs +++ b/src/Twilio.AspNet.Mvc.UnitTests/RequestValidationHelperTests.cs @@ -1,66 +1,67 @@ using System.Collections.Specialized; using Xunit; -namespace Twilio.AspNet.Mvc.UnitTests +namespace Twilio.AspNet.Mvc.UnitTests; + +public class RequestValidationHelperTests { - public class RequestValidationHelperTests + [Fact] + public void TestLocal() { - [Fact] - public void TestLocal() - { - var fakeContext = (new ContextMocks(true)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", true); + var fakeContext = new ContextMocks(true).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", true); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public void TestNoLocal() - { - var fakeContext = (new ContextMocks(true)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", false); + [Fact] + public void TestNoLocal() + { + var fakeContext = new ContextMocks(true).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token"); - Assert.False(result); - } + Assert.False(result); + } - [Fact] - public void TestNoForm() - { - var fakeContext = (new ContextMocks(true)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false); + [Fact] + public void TestNoForm() + { + var fakeContext = new ContextMocks(true).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public void TestUrlOverrideFail() - { - var fakeContext = (new ContextMocks(true)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", false); + [Fact] + public void TestUrlOverrideFail() + { + var fakeContext = new ContextMocks(true).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken, "https://example.com/"); - Assert.False(result); - } + Assert.False(result); + } - [Fact] - public void TestUrlOverride() - { - var fakeContext = (new ContextMocks("https://example.com/", true)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", false); + [Fact] + public void TestUrlOverride() + { + var fakeContext = new ContextMocks("https://example.com/", true).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken, "https://example.com/"); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public void TestForm() + [Fact] + public void TestForm() + { + var form = new NameValueCollection { - var form = new NameValueCollection(); - form.Add("key1", "value1"); - form.Add("key2", "value2"); - var fakeContext = (new ContextMocks(true, form)).HttpContext.Object; - var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false); + { "key1", "value1" }, + { "key2", "value2" } + }; + var fakeContext = new ContextMocks(true, form).HttpContext.Object; + var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.FakeAuthToken); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs b/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs index 91ae7a8..82dcd6e 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs +++ b/src/Twilio.AspNet.Mvc.UnitTests/TwiMLResultTests.cs @@ -1,77 +1,74 @@ -using System; -using System.IO; -using System.Xml.Linq; +using System.Xml.Linq; using Twilio.TwiML; using Xunit; -namespace Twilio.AspNet.Mvc.UnitTests +namespace Twilio.AspNet.Mvc.UnitTests; + +public class TwiMLResultTest { - public class TwiMLResultTest - { - private readonly ContextMocks mocks = new ContextMocks(true); - private static readonly string NewLine = Environment.NewLine; + private readonly ContextMocks _mocks = new(true); + private static readonly string NewLine = Environment.NewLine; - [Fact] - public void TestVoiceResponse() - { - var response = new VoiceResponse().Say("Ahoy!"); + [Fact] + public void TestVoiceResponse() + { + var response = new VoiceResponse().Say("Ahoy!"); - var result = new TwiMLResult(response); - result.ExecuteResult(mocks.ControllerContext.Object); + var result = new TwiMLResult(response); + result.ExecuteResult(_mocks.ControllerContext.Object); - Assert.Equal($"{NewLine}" + - $"{NewLine}" + - $" Ahoy!{NewLine}" + - "", - mocks.Response.Object.Output.ToString() - ); - } + Assert.Equal($"{NewLine}" + + $"{NewLine}" + + $" Ahoy!{NewLine}" + + "", + _mocks.Response.Object.Output.ToString() + ); + } - [Fact] - public void TestVoiceResponseUnformatted() - { - var response = new VoiceResponse().Say("Ahoy!"); + [Fact] + public void TestVoiceResponseUnformatted() + { + var response = new VoiceResponse().Say("Ahoy!"); - var result = new TwiMLResult(response, SaveOptions.DisableFormatting); - result.ExecuteResult(mocks.ControllerContext.Object); + var result = new TwiMLResult(response, SaveOptions.DisableFormatting); + result.ExecuteResult(_mocks.ControllerContext.Object); - Assert.Equal("" + - "" + - "Ahoy!" + - "", - mocks.Response.Object.Output.ToString() - ); - } + Assert.Equal("" + + "" + + "Ahoy!" + + "", + _mocks.Response.Object.Output.ToString() + ); + } - [Fact] - public void TestVoiceResponseUnformattedUtf16() - { - // string writer has Utf16 encoding - mocks.Response.Setup(r => r.Output).Returns(new StringWriter()); - var response = new VoiceResponse().Say("Ahoy!"); + [Fact] + public void TestVoiceResponseUnformattedUtf16() + { + // 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); + var result = new TwiMLResult(response, SaveOptions.DisableFormatting); + result.ExecuteResult(_mocks.ControllerContext.Object); - Assert.Equal("" + - "" + - "Ahoy!" + - "", - mocks.Response.Object.Output.ToString() - ); - } + Assert.Equal("" + + "" + + "Ahoy!" + + "", + _mocks.Response.Object.Output.ToString() + ); + } - [Fact] - public void TestNullTwiml() - { - var result = new TwiMLResult(null); - result.ExecuteResult(mocks.ControllerContext.Object); + [Fact] + public void TestNullTwiml() + { + var result = new TwiMLResult(null!); + result.ExecuteResult(_mocks.ControllerContext.Object); - Assert.Equal( - "", - mocks.Response.Object.Output.ToString() - ); - mocks.Response.Object.Output.Dispose(); - } + 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/Twilio.AspNet.Mvc.UnitTests.csproj b/src/Twilio.AspNet.Mvc.UnitTests/Twilio.AspNet.Mvc.UnitTests.csproj index 74ad47f..66516de 100644 --- a/src/Twilio.AspNet.Mvc.UnitTests/Twilio.AspNet.Mvc.UnitTests.csproj +++ b/src/Twilio.AspNet.Mvc.UnitTests/Twilio.AspNet.Mvc.UnitTests.csproj @@ -3,6 +3,10 @@ Library net471 + false + 13 + enable + enable @@ -25,20 +29,27 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Twilio.AspNet.Mvc/RequestValidationHelper.cs b/src/Twilio.AspNet.Mvc/RequestValidationHelper.cs index 577bf5a..c8a0a6c 100644 --- a/src/Twilio.AspNet.Mvc/RequestValidationHelper.cs +++ b/src/Twilio.AspNet.Mvc/RequestValidationHelper.cs @@ -1,54 +1,52 @@ using System.Collections.Specialized; -using System.Linq; using System.Web; using Twilio.Security; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +/// +/// Class used to validate incoming requests from Twilio using 'Request Validation' as described +/// in the Security section of the Twilio TwiML API documentation. +/// +public static class RequestValidationHelper { /// - /// Class used to validate incoming requests from Twilio using 'Request Validation' as described - /// in the Security section of the Twilio TwiML API documentation. + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute /// - public static class RequestValidationHelper - { - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// AuthToken for the account used to sign the request - /// - /// Skip validation for local requests. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static bool IsValidRequest(HttpContextBase context, string authToken, bool allowLocal = false) - => IsValidRequest(context, authToken, null, allowLocal); + /// HttpContext to use for validation + /// AuthToken for the account used to sign the request + /// + /// Skip validation for local requests. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static bool IsValidRequest(HttpContextBase context, string authToken, bool allowLocal = false) + => IsValidRequest(context, authToken, null, allowLocal); - /// - /// Performs request validation using the current HTTP context passed in manually or from - /// the ASP.NET MVC ValidateRequestAttribute - /// - /// HttpContext to use for validation - /// 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. - /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. - /// - public static bool IsValidRequest(HttpContextBase context, string authToken, string urlOverride, bool allowLocal = false) + /// + /// Performs request validation using the current HTTP context passed in manually or from + /// the ASP.NET MVC ValidateRequestAttribute + /// + /// HttpContext to use for validation + /// 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. + /// ⚠️ Only use this during development, as this will make your application vulnerable to Server-Side Request Forgery. + /// + public static bool IsValidRequest(HttpContextBase context, string authToken, string? urlOverride, bool allowLocal = false) + { + if (allowLocal && context.Request.IsLocal && !context.Request.Headers.AllKeys.Contains("X-Forwarded-For")) { - if (allowLocal && context.Request.IsLocal && !context.Request.Headers.AllKeys.Contains("X-Forwarded-For")) - { - return true; - } - - var fullUrl = string.IsNullOrEmpty(urlOverride) ? context.Request.Url?.AbsoluteUri : urlOverride; - var validator = new RequestValidator(authToken); - return validator.Validate( - url: fullUrl, - parameters: context.Request?.Form ?? new NameValueCollection(), - expected: context.Request?.Headers["X-Twilio-Signature"] - ); + return true; } + + var fullUrl = string.IsNullOrEmpty(urlOverride) ? context.Request.Url?.AbsoluteUri : urlOverride; + var validator = new RequestValidator(authToken); + return validator.Validate( + url: fullUrl, + parameters: context.Request.Form ?? new NameValueCollection(), + expected: context.Request.Headers["X-Twilio-Signature"] + ); } -} +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/TwiMLResult.cs b/src/Twilio.AspNet.Mvc/TwiMLResult.cs index 76296bd..7d297ff 100644 --- a/src/Twilio.AspNet.Mvc/TwiMLResult.cs +++ b/src/Twilio.AspNet.Mvc/TwiMLResult.cs @@ -1,42 +1,41 @@ using System.Web.Mvc; using System.Xml.Linq; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +public class TwiMLResult : ActionResult { - public class TwiMLResult : ActionResult + private readonly SaveOptions _formattingOptions; + private readonly TwiML.TwiML _dataTwiml; + + public TwiMLResult(TwiML.TwiML response) : this(response, SaveOptions.None) { - private readonly SaveOptions formattingOptions; - private readonly TwiML.TwiML dataTwiml; + } - public TwiMLResult(TwiML.TwiML response) : this(response, SaveOptions.None) - { - } + public TwiMLResult(TwiML.TwiML response, SaveOptions formattingOptions) + { + _dataTwiml = response; + _formattingOptions = formattingOptions; + } - public TwiMLResult(TwiML.TwiML response, SaveOptions formattingOptions) + public override void ExecuteResult(ControllerContext controllerContext) + { + var response = controllerContext.HttpContext.Response; + var encoding = response.Output.Encoding.BodyName; + response.ContentType = "application/xml"; + + if (_dataTwiml is null) { - this.dataTwiml = response; - this.formattingOptions = formattingOptions; + response.Output.Write($""); + return; } - public override void ExecuteResult(ControllerContext controllerContext) + var twimlString = _dataTwiml.ToString(_formattingOptions); + if (encoding != "utf-8") { - var response = controllerContext.HttpContext.Response; - var encoding = response.Output.Encoding.BodyName; - response.ContentType = "application/xml"; - - if (dataTwiml == null) - { - response.Output.Write($""); - return; - } - - var twimlString = dataTwiml.ToString(formattingOptions); - if (encoding != "utf-8") - { - twimlString = twimlString.Replace("utf-8", encoding); - } - - response.Output.Write(twimlString); + twimlString = twimlString.Replace("utf-8", encoding); } + + response.Output.Write(twimlString); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/Twilio.AspNet.Mvc.csproj b/src/Twilio.AspNet.Mvc/Twilio.AspNet.Mvc.csproj index f2403a6..3d5f583 100644 --- a/src/Twilio.AspNet.Mvc/Twilio.AspNet.Mvc.csproj +++ b/src/Twilio.AspNet.Mvc/Twilio.AspNet.Mvc.csproj @@ -3,18 +3,23 @@ Library net462 + 13 + enable + enable 0.0.0-alpha Twilio.AspNet.Mvc 0.0.0-alpha Twilio Labs + Twilio helper library for ASP.NET MVC Twilio helper library for ASP.NET MVC on .NET Framework. false Refer to the changelog at https://github.com/twilio-labs/twilio-aspnet/blob/main/CHANGELOG.md - Copyright 2022 (c) Twilio, Inc. All rights reserved. + Copyright 2024 (c) Twilio, Inc. All rights reserved. twilio;twiml;sms;voice;telephony;phone;aspnet Apache-2.0 https://github.com/twilio-labs/twilio-aspnet https://github.com/twilio-labs/twilio-aspnet.git + git https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/twilio-icon-64x64.png icon.png README.md @@ -29,9 +34,7 @@ - - ..\..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Configuration.dll - + @@ -49,14 +52,18 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all diff --git a/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs b/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs index af4b365..35468f9 100644 --- a/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs +++ b/src/Twilio.AspNet.Mvc/TwilioConfiguration.cs @@ -1,36 +1,35 @@ using System.Configuration; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +public class RequestValidationConfigurationSection : ConfigurationSection { - public class RequestValidationConfigurationSection : ConfigurationSection + [ConfigurationProperty("authToken")] + public string AuthToken { - [ConfigurationProperty("authToken")] - public string AuthToken - { - get => (string)this["authToken"]; - set => this["authToken"] = value; - } - - [ConfigurationProperty("baseUrlOverride")] - public string BaseUrlOverride - { - get => (string)this["baseUrlOverride"]; - set => this["baseUrlOverride"] = value; - } - - [ConfigurationProperty("allowLocal")] - public bool AllowLocal - { - get => (bool)this["allowLocal"]; - set => this["allowLocal"] = value; - } + get => (string)this["authToken"]; + set => this["authToken"] = value; } - public class TwilioSectionGroup : ConfigurationSectionGroup + [ConfigurationProperty("baseUrlOverride")] + public string BaseUrlOverride { + get => (string)this["baseUrlOverride"]; + set => this["baseUrlOverride"] = value; + } - [ConfigurationProperty("requestValidation", IsRequired = false)] - public RequestValidationConfigurationSection RequestValidation - => (RequestValidationConfigurationSection)Sections["requestValidation"]; + [ConfigurationProperty("allowLocal")] + public bool AllowLocal + { + get => (bool)this["allowLocal"]; + set => this["allowLocal"] = value; } +} + +public class TwilioSectionGroup : ConfigurationSectionGroup +{ + + [ConfigurationProperty("requestValidation", IsRequired = false)] + public RequestValidationConfigurationSection RequestValidation + => (RequestValidationConfigurationSection)Sections["requestValidation"]; } \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/TwilioController.cs b/src/Twilio.AspNet.Mvc/TwilioController.cs index 6a90921..305ae78 100644 --- a/src/Twilio.AspNet.Mvc/TwilioController.cs +++ b/src/Twilio.AspNet.Mvc/TwilioController.cs @@ -2,45 +2,44 @@ using System.Xml.Linq; using Twilio.TwiML; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +/// +/// Extends the standard base controller to simplify returning a TwiML response +/// +public class TwilioController : Controller { - /// - /// Extends the standard base controller to simplify returning a TwiML response - /// - public class TwilioController : Controller - { - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - public TwiMLResult TwiML(MessagingResponse response) - => new TwiMLResult(response); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + public TwiMLResult TwiML(MessagingResponse response) + => new(response); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - public TwiMLResult TwiML(MessagingResponse response, SaveOptions formattingOptions) - => new TwiMLResult(response, formattingOptions); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + public TwiMLResult TwiML(MessagingResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - public TwiMLResult TwiML(VoiceResponse response) - => new TwiMLResult(response); + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + public TwiMLResult TwiML(VoiceResponse response) + => new(response); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) - => new TwiMLResult(response, formattingOptions); - } -} + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + public TwiMLResult TwiML(VoiceResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs b/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs index 06bc669..7b9b914 100644 --- a/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs +++ b/src/Twilio.AspNet.Mvc/TwilioControllerExtensions.cs @@ -2,49 +2,48 @@ using System.Xml.Linq; using Twilio.TwiML; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +/// +/// Adds extension methods to the ControllerBase class for returning TwiML in MVC actions +/// +public static class TwilioControllerExtensions { /// - /// Adds extension methods to the ControllerBase class for returning TwiML in MVC actions + /// Returns a properly formatted TwiML response /// - public static class TwilioControllerExtensions - { - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) - => new TwiMLResult(response); + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response) + => new(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 + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, MessagingResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - 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) + => new(response); - /// - /// Returns a properly formatted TwiML response - /// - /// - /// - /// - /// - public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) - => new TwiMLResult(response, formattingOptions); - } -} + /// + /// Returns a properly formatted TwiML response + /// + /// + /// + /// + /// + public static TwiMLResult TwiML(this ControllerBase controller, VoiceResponse response, SaveOptions formattingOptions) + => new(response, formattingOptions); +} \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs b/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs index d022f6f..4e0d219 100644 --- a/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs +++ b/src/Twilio.AspNet.Mvc/TwilioUriHelper.cs @@ -1,11 +1,9 @@ -using System; -using System.Web.Mvc; +using System.Web.Mvc; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +public static class TwilioUriHelper { - public static class TwilioUriHelper - { - public static Uri ActionUri(this UrlHelper helper, string actionName, string controllerName) - => new Uri(helper.Action(actionName, controllerName), UriKind.Relative); - } + public static Uri ActionUri(this UrlHelper helper, string actionName, string controllerName) => + new(helper.Action(actionName, controllerName), UriKind.Relative); } \ No newline at end of file diff --git a/src/Twilio.AspNet.Mvc/ValidateRequestAttribute.cs b/src/Twilio.AspNet.Mvc/ValidateRequestAttribute.cs index d4ce5f4..b4494f1 100644 --- a/src/Twilio.AspNet.Mvc/ValidateRequestAttribute.cs +++ b/src/Twilio.AspNet.Mvc/ValidateRequestAttribute.cs @@ -1,73 +1,71 @@ -using System; -using System.Configuration; +using System.Configuration; using System.Net; using System.Web.Mvc; -namespace Twilio.AspNet.Mvc +namespace Twilio.AspNet.Mvc; + +/// +/// Represents an attribute that is used to prevent forgery of a request. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Module)] +public class ValidateRequestAttribute : ActionFilterAttribute { + protected internal string? AuthToken { get; set; } + protected internal string? BaseUrlOverride { get; set; } + protected internal bool AllowLocal { get; set; } + /// - /// Represents an attribute that is used to prevent forgery of a request. + /// Initializes a new instance of the ValidateRequestAttribute class. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Module)] - public class ValidateRequestAttribute : ActionFilterAttribute + public ValidateRequestAttribute() { - protected internal string AuthToken { get; set; } - protected internal string BaseUrlOverride { get; set; } - protected internal bool AllowLocal { get; set; } + ConfigureProperties(); + } - /// - /// Initializes a new instance of the ValidateRequestAttribute class. - /// - public ValidateRequestAttribute() - { - ConfigureProperties(); - } + /// + /// Configures the properties of the attribute. + /// + /// + /// This method exists so ValidateRequestAttribute can be inherited from + /// and ConfigureProperties can be overridden. + /// + /// + protected virtual void ConfigureProperties() + { + var requestValidationConfiguration = + ConfigurationManager.GetSection("twilio/requestValidation") as RequestValidationConfigurationSection; + var appSettings = ConfigurationManager.AppSettings; - /// - /// Configures the properties of the attribute. - /// - /// - /// This method exists so ValidateRequestAttribute can be inherited from - /// and ConfigureProperties can be overridden. - /// - /// - protected virtual void ConfigureProperties() - { - var requestValidationConfiguration = - ConfigurationManager.GetSection("twilio/requestValidation") as RequestValidationConfigurationSection; - var appSettings = ConfigurationManager.AppSettings; + AuthToken = appSettings["twilio:requestValidation:authToken"] + ?? appSettings["twilio:authToken"] + ?? requestValidationConfiguration?.AuthToken + ?? throw new Exception("Twilio Auth Token not configured"); - AuthToken = appSettings["twilio:requestValidation:authToken"] - ?? appSettings["twilio:authToken"] - ?? requestValidationConfiguration?.AuthToken - ?? throw new Exception("Twilio Auth Token not configured"); + BaseUrlOverride = (appSettings["twilio:requestValidation:baseUrlOverride"] + ?? requestValidationConfiguration?.BaseUrlOverride) + ?.TrimEnd('/'); - BaseUrlOverride = (appSettings["twilio:requestValidation:baseUrlOverride"] - ?? requestValidationConfiguration?.BaseUrlOverride) - ?.TrimEnd('/'); + var allowLocalAppSetting = appSettings["twilio:requestValidation:allowLocal"]; + AllowLocal = allowLocalAppSetting is not null + ? bool.Parse(allowLocalAppSetting) + : requestValidationConfiguration?.AllowLocal + ?? false; + } - var allowLocalAppSetting = appSettings["twilio:requestValidation:allowLocal"]; - AllowLocal = allowLocalAppSetting != null - ? bool.Parse(allowLocalAppSetting) - : requestValidationConfiguration?.AllowLocal - ?? false; + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + var httpContext = filterContext.HttpContext; + string? urlOverride = null; + if (BaseUrlOverride is not null) + { + urlOverride = $"{BaseUrlOverride}{httpContext.Request.Path}{httpContext.Request.QueryString}"; } - public override void OnActionExecuting(ActionExecutingContext filterContext) + if (!RequestValidationHelper.IsValidRequest(filterContext.HttpContext, AuthToken!, urlOverride, AllowLocal)) { - var httpContext = filterContext.HttpContext; - string urlOverride = null; - if (BaseUrlOverride != null) - { - urlOverride = $"{BaseUrlOverride}{httpContext.Request.Path}{httpContext.Request.QueryString}"; - } - - if (!RequestValidationHelper.IsValidRequest(filterContext.HttpContext, AuthToken, urlOverride, AllowLocal)) - { - filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden); - } - - base.OnActionExecuting(filterContext); + filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden); } + + base.OnActionExecuting(filterContext); } } \ No newline at end of file diff --git a/src/Twilio.AspNet.sln b/src/Twilio.AspNet.sln index e5d2ada..386ed3b 100644 --- a/src/Twilio.AspNet.sln +++ b/src/Twilio.AspNet.sln @@ -31,6 +31,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilio.AspNet.Mvc.UnitTests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Twilio.AspNet.Core.UnitTests", "Twilio.AspNet.Core.UnitTests\Twilio.AspNet.Core.UnitTests.csproj", "{B3E732C9-27EF-4E96-B620-5B5DA57D9AD3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{14C361F2-74BC-4D62-AC32-8B640D28A94F}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\release.yml = ..\.github\workflows\release.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{5001EA6E-22E1-454C-8746-D76CD562D89B}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,4 +74,8 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7D0F9171-129A-4B05-809E-F501DBC23197} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {14C361F2-74BC-4D62-AC32-8B640D28A94F} = {339A75DF-3315-4CF3-9FD9-DAF1B0DB50B2} + {5001EA6E-22E1-454C-8746-D76CD562D89B} = {14C361F2-74BC-4D62-AC32-8B640D28A94F} + EndGlobalSection EndGlobal diff --git a/src/testapps/AspNetFramework/AspNetFramework.csproj b/src/testapps/AspNetFramework/AspNetFramework.csproj index 8d2fc2b..da3c569 100644 --- a/src/testapps/AspNetFramework/AspNetFramework.csproj +++ b/src/testapps/AspNetFramework/AspNetFramework.csproj @@ -42,8 +42,9 @@ 4 - - ..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + + ..\packages\Microsoft.Bcl.AsyncInterfaces.9.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll + True ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.3.6.0\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll @@ -72,8 +73,13 @@ ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - ..\packages\System.Text.Encodings.Web.7.0.0\lib\net462\System.Text.Encodings.Web.dll + + ..\packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll + True + + + ..\packages\System.Text.Json.8.0.5\lib\net462\System.Text.Json.dll + True ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll diff --git a/src/testapps/AspNetFramework/Web.config b/src/testapps/AspNetFramework/Web.config index 770e76c..8857f51 100644 --- a/src/testapps/AspNetFramework/Web.config +++ b/src/testapps/AspNetFramework/Web.config @@ -45,15 +45,15 @@ - + - + - + @@ -73,7 +73,7 @@ - + @@ -85,7 +85,7 @@ - + diff --git a/src/testapps/AspNetFramework/packages.config b/src/testapps/AspNetFramework/packages.config index e78ce96..710c3ec 100644 --- a/src/testapps/AspNetFramework/packages.config +++ b/src/testapps/AspNetFramework/packages.config @@ -4,7 +4,7 @@ - + @@ -13,8 +13,8 @@ - - + + diff --git a/version_bump.ps1 b/version_bump.ps1 old mode 100644 new mode 100755 index 415ecd0..7692645 --- a/version_bump.ps1 +++ b/version_bump.ps1 @@ -1,6 +1,7 @@ +#!/usr/bin/env pwsh function updateStandardCsproj() { Param($inputFileNameRelative, $targetVersion) - + Write-Host "Updating : $inputFileNameRelative" $inputFileName = Join-Path $PSScriptRoot $inputFileNameRelative