diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index fcb1eda744b..567a98ebfb9 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -351,6 +351,12 @@ public virtual string CustomMethod private string _custommethod; + /// + /// Gets or sets the PreserveHttpMethodOnRedirect property. + /// + [Parameter] + public virtual SwitchParameter PreserveHttpMethodOnRedirect { get; set; } + #endregion Method #region NoProxy @@ -509,7 +515,7 @@ protected override void ProcessRecord() bool keepAuthorizationOnRedirect = PreserveAuthorizationOnRedirect.IsPresent && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization); - bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect; + bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHttpMethodOnRedirect; using (HttpClient client = GetHttpClient(handleRedirect)) { @@ -1239,9 +1245,8 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM } // For selected redirects, GET must be used with the redirected Location. - if (currentRequest.Method == HttpMethod.Post && IsRedirectToGet(response.StatusCode)) + if (RequestRequiresForceGet(response.StatusCode, currentRequest.Method) && !PreserveHttpMethodOnRedirect) { - // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx Method = WebRequestMethod.Get; CustomMethod = string.Empty; } @@ -1713,7 +1718,7 @@ private static StreamContent GetMultipartStreamContent(object fieldName, Stream private static StreamContent GetMultipartFileContent(object fieldName, FileInfo file) { StreamContent result = GetMultipartStreamContent(fieldName: fieldName, stream: new FileStream(file.FullName, FileMode.Open)); - + // .NET does not enclose field names in quotes, however, modern browsers and curl do. result.Headers.ContentDisposition.FileName = "\"" + file.Name + "\""; @@ -1773,43 +1778,34 @@ private static string FormatErrorMessage(string error, string contentType) } // Returns true if the status code is one of the supported redirection codes. - private static bool IsRedirectCode(HttpStatusCode code) + private static bool IsRedirectCode(HttpStatusCode statusCode) => statusCode switch { - int intCode = (int)code; - return - ( - (intCode >= 300 && intCode < 304) || - intCode == 307 || - intCode == 308 - ); - } + HttpStatusCode.Found + or HttpStatusCode.Moved + or HttpStatusCode.MultipleChoices + or HttpStatusCode.PermanentRedirect + or HttpStatusCode.SeeOther + or HttpStatusCode.TemporaryRedirect => true, + _ => false + }; - // Returns true if the status code is a redirection code and the action requires switching from POST to GET on redirection. - // NOTE: Some of these status codes map to the same underlying value but spelling them out for completeness. - private static bool IsRedirectToGet(HttpStatusCode code) + // Returns true if the status code is a redirection code and the action requires switching to GET on redirection. + // See https://learn.microsoft.com/en-us/dotnet/api/system.net.httpstatuscode + private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) => statusCode switch { - return - ( - code == HttpStatusCode.Found || - code == HttpStatusCode.Moved || - code == HttpStatusCode.Redirect || - code == HttpStatusCode.RedirectMethod || - code == HttpStatusCode.SeeOther || - code == HttpStatusCode.Ambiguous || - code == HttpStatusCode.MultipleChoices - ); - } + HttpStatusCode.Found + or HttpStatusCode.Moved + or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post, + HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head, + _ => false + }; // Returns true if the status code shows a server or client error and MaximumRetryCount > 0 - private bool ShouldRetry(HttpStatusCode code) + private static bool ShouldRetry(HttpStatusCode statusCode) => (int)statusCode switch { - int intCode = (int)code; - - return - ( - (intCode == 304 || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 0 - ); - } + 304 or (>= 400 and <= 599) => true, + _ => false + }; private static HttpMethod GetHttpMethod(WebRequestMethod method) => method switch { diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 3538a799b9b..2da345fcd3a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -139,6 +139,9 @@ function ExecuteRedirectRequest { [switch] $PreserveAuthorizationOnRedirect, + [switch] + $PreserveHttpMethodOnRedirect, + [ValidateRange(0, [int]::MaxValue)] [int] $MaximumRedirection @@ -153,7 +156,7 @@ function ExecuteRedirectRequest { } elseif ($CustomMethod) { $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -CustomMethod $CustomMethod } else { - $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method + $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -PreserveHttpMethodOnRedirect:$PreserveHttpMethodOnRedirect.IsPresent -Method $Method } $result.Content = $result.Output.Content | ConvertFrom-Json } else { @@ -162,7 +165,7 @@ function ExecuteRedirectRequest { } elseif ($CustomMethod) { $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -CustomMethod $CustomMethod } else { - $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method + $result.Output = Invoke-RestMethod -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -PreserveHttpMethodOnRedirect:$PreserveHttpMethodOnRedirect.IsPresent -Method $Method } # NOTE: $result.Output should already be a PSObject (Invoke-RestMethod converts the returned json automatically) # so simply reference $result.Output @@ -1023,6 +1026,20 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $response.Content.Method | Should -Be $redirectedMethod } + It "Validates Invoke-WebRequest -PreserveHttpMethodOnRedirect keeps the authorization header redirects and do remains POST when it handles the redirect: " -TestCases $redirectTests { + param($redirectType) + $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} + $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Uri $uri -Method 'POST' + + $response.Error | Should -BeNullOrEmpty + # ensure user-agent is present (i.e., no false positives ) + $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty + # ensure Authorization header has been kept. + $response.Content.Headers."Authorization" | Should -BeExactly 'test' + # ensure POST doesn't change. + $response.Content.Method | Should -Be 'POST' + } + It "Validates Invoke-WebRequest handles responses without Location header for requests with Authorization header and redirect: " -TestCases $redirectTests { param($redirectType, $redirectedMethod) # Skip relative test as it is not a valid response type. @@ -2745,6 +2762,20 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $response.Content.Method | Should -Be $redirectedMethod } + It "Validates Invoke-RestMethod -PreserveHttpMethodOnRedirect keeps the authorization header redirects and remains POST when it handles the redirect: " -TestCases $redirectTests { + param($redirectType) + $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} + $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' + + $response.Error | Should -BeNullOrEmpty + # ensure user-agent is present (i.e., no false positives ) + $response.Content.Headers."User-Agent" | Should -Not -BeNullOrEmpty + # ensure Authorization header has been kept. + $response.Content.Headers."Authorization" | Should -BeExactly 'test' + # ensure POST doesn't change. + $response.Content.Method | Should -Be 'POST' + } + It "Validates Invoke-RestMethod handles responses without Location header for requests with Authorization header and redirect: " -TestCases $redirectTests { param($redirectType, $redirectedMethod) # Skip relative test as it is not a valid response type.