From b4467aa9da02df9aa0aaa59c9cc0ac9f77043419 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:31:50 +0100 Subject: [PATCH 01/27] Add PersistHTTPMethod parameter --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 7 +++++++ 1 file changed, 7 insertions(+) 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 e82bd7e5772..fde87624685 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 @@ -273,6 +273,13 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet [ValidateNotNullOrEmpty] public virtual string CustomMethod { get; set; } + /// + /// Gets or sets the PersistHTTPMethod property. + /// + [Parameter] + [ValidateSet("All", "300", "301", "302", "303", IgnoreCase = true)] + public virtual string[] PersistHTTPMethod { get; set; } + #endregion #region NoProxy From 720ece84c6afd42f8f4fb592785a2882f7ff861a Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:33:05 +0100 Subject: [PATCH 02/27] PersistHTTPMethod handle redirect --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fde87624685..138caa67e19 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 @@ -975,7 +975,7 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) } // This indicates GetResponse will handle redirects. - if (handleRedirect) + if (handleRedirect || PersistHTTPMethod is not null) { handler.AllowAutoRedirect = false; } From 20520e68145e4055a16761ae8b3fa2dc0ba78a36 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:38:48 +0100 Subject: [PATCH 03/27] PersistHTTPMethod manual redirect --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 138caa67e19..d74ad09d61f 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 @@ -1319,8 +1319,10 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM _cancelToken = new CancellationTokenSource(); response = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult(); + + bool sessionRedirect = WebSession.MaximumRedirection > 0 || WebSession.MaximumRedirection == -1; - if (keepAuthorization && IsRedirectCode(response.StatusCode) && response.Headers.Location is not null) + if ((keepAuthorization || (PersistHTTPMethod is not null && sessionRedirect)) && IsRedirectCode(response.StatusCode) && response.Headers.Location is not null) { _cancelToken.Cancel(); _cancelToken = null; From 1335810665e11bc1859401cdc6bfdd42d72da419 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:46:42 +0100 Subject: [PATCH 04/27] replace IsRedirectToGet with RequestRequiresForceGet from dotnet --- .../Common/WebRequestPSCmdlet.Common.cs | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) 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 d74ad09d61f..b4a15f0e222 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 @@ -1274,20 +1274,20 @@ private static bool IsRedirectCode(HttpStatusCode code) ); } - // 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. + private static bool RequestRequiresForceGet(HttpStatusCode statusCode, WebRequestMethod requestMethod) { - return - ( - code == HttpStatusCode.Found || - code == HttpStatusCode.Moved || - code == HttpStatusCode.Redirect || - code == HttpStatusCode.RedirectMethod || - code == HttpStatusCode.SeeOther || - code == HttpStatusCode.Ambiguous || - code == HttpStatusCode.MultipleChoices - ); + switch (statusCode) + { + case HttpStatusCode.Moved: + case HttpStatusCode.Found: + case HttpStatusCode.MultipleChoices: + return requestMethod == WebRequestMethod.Post; + case HttpStatusCode.SeeOther: + return requestMethod != WebRequestMethod.Get && requestMethod != WebRequestMethod.Head; + default: + return false; + } } // Returns true if the status code shows a server or client error and MaximumRetryCount > 0 @@ -1327,17 +1327,16 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM _cancelToken.Cancel(); _cancelToken = null; - // if explicit count was provided, reduce it for this redirection. + // If explicit count was provided, reduce it for this redirection. if (WebSession.MaximumRedirection > 0) { WebSession.MaximumRedirection--; } - // For selected redirects that used POST, GET must be used with the - // redirected Location. - // Since GET is the default; POST only occurs when -Method POST is used. - if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode)) + + // For selected redirects, GET must be used with the redirected Location. + // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx + if (RequestRequiresForceGet(response.StatusCode, Method)) { - // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx Method = WebRequestMethod.Get; } From a36718ca0660b56c08fba35b990b8189ab541599 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:54:13 +0100 Subject: [PATCH 05/27] Update IsRedirectCode with code from RedirectHandler.cs --- .../Common/WebRequestPSCmdlet.Common.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 b4a15f0e222..921c659267d 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 @@ -1265,13 +1265,18 @@ internal virtual void FillRequestStream(HttpRequestMessage request) // Returns true if the status code is one of the supported redirection codes. private static bool IsRedirectCode(HttpStatusCode code) { - int intCode = (int)code; - return - ( - (intCode >= 300 && intCode < 304) || - intCode == 307 || - intCode == 308 - ); + switch (statusCode) + { + case HttpStatusCode.Moved: + case HttpStatusCode.Found: + case HttpStatusCode.SeeOther: + case HttpStatusCode.TemporaryRedirect: + case HttpStatusCode.MultipleChoices: + case HttpStatusCode.PermanentRedirect: + return true; + default: + return false; + } } // Returns true if the status code is a redirection code and the action requires switching to GET on redirection. From 949954832ba0a7630b717489a3877de2abe40ae7 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 00:59:49 +0100 Subject: [PATCH 06/27] Add PersistHTTPMethod --- .../Common/WebRequestPSCmdlet.Common.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 921c659267d..dec8f857269 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 @@ -1341,6 +1341,23 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // For selected redirects, GET must be used with the redirected Location. // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx if (RequestRequiresForceGet(response.StatusCode, Method)) + { + if (PersistHTTPMethod is not null) + { + switch (response.StatusCode) + { + case HttpStatusCode.Moved when Array.Exists(PersistHTTPMethod, element => element == "301" || element == "All"): + case HttpStatusCode.Found when Array.Exists(PersistHTTPMethod, element => element == "302" || element == "All"): + case HttpStatusCode.SeeOther when Array.Exists(PersistHTTPMethod, element => element == "303" || element == "All"): + case HttpStatusCode.MultipleChoices when Array.Exists(PersistHTTPMethod, element => element == "300" || element == "All"): + break; + default: + Method = WebRequestMethod.Get; + break; + } + } + } + else { Method = WebRequestMethod.Get; } From 689455b44fea93ff69a7737d0ad184e334df43aa Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 01:23:15 +0100 Subject: [PATCH 07/27] Fix error --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dec8f857269..f17cea63f8b 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 @@ -1263,7 +1263,7 @@ internal virtual void FillRequestStream(HttpRequestMessage request) } // 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) { switch (statusCode) { From 25ee798401c74c385f647d966135008ba94cd8bb Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 01:48:26 +0100 Subject: [PATCH 08/27] fix test error (pasted else in the wrong place) --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 f17cea63f8b..7c808d2788e 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 @@ -1356,10 +1356,10 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM break; } } - } - else - { - Method = WebRequestMethod.Get; + else + { + Method = WebRequestMethod.Get; + } } currentUri = new Uri(request.RequestUri, response.Headers.Location); From 5278cf78441eb1d451e1c18c36c303d5163a9603 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 18:45:52 +0100 Subject: [PATCH 09/27] Convert to SwitchParameter and PreserveHTTPMethodOnRedirect --- .../Common/WebRequestPSCmdlet.Common.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 7c808d2788e..b399d2bccd9 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 @@ -274,11 +274,10 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet public virtual string CustomMethod { get; set; } /// - /// Gets or sets the PersistHTTPMethod property. + /// Gets or sets the PreserveHTTPMethodOnRedirect property. /// [Parameter] - [ValidateSet("All", "300", "301", "302", "303", IgnoreCase = true)] - public virtual string[] PersistHTTPMethod { get; set; } + public virtual SwitchParameter PreserveHTTPMethodOnRedirect { get; set; } #endregion @@ -975,7 +974,7 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) } // This indicates GetResponse will handle redirects. - if (handleRedirect || PersistHTTPMethod is not null) + if (handleRedirect || PreserveHTTPMethodOnRedirect) { handler.AllowAutoRedirect = false; } @@ -1327,7 +1326,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM bool sessionRedirect = WebSession.MaximumRedirection > 0 || WebSession.MaximumRedirection == -1; - if ((keepAuthorization || (PersistHTTPMethod is not null && sessionRedirect)) && IsRedirectCode(response.StatusCode) && response.Headers.Location is not null) + if ((keepAuthorization || (PreserveHTTPMethodOnRedirect && sessionRedirect)) && IsRedirectCode(response.StatusCode) && response.Headers.Location is not null) { _cancelToken.Cancel(); _cancelToken = null; @@ -1342,14 +1341,14 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx if (RequestRequiresForceGet(response.StatusCode, Method)) { - if (PersistHTTPMethod is not null) + if (PreserveHTTPMethodOnRedirect) { switch (response.StatusCode) { - case HttpStatusCode.Moved when Array.Exists(PersistHTTPMethod, element => element == "301" || element == "All"): - case HttpStatusCode.Found when Array.Exists(PersistHTTPMethod, element => element == "302" || element == "All"): - case HttpStatusCode.SeeOther when Array.Exists(PersistHTTPMethod, element => element == "303" || element == "All"): - case HttpStatusCode.MultipleChoices when Array.Exists(PersistHTTPMethod, element => element == "300" || element == "All"): + case HttpStatusCode.Moved: + case HttpStatusCode.Found: + case HttpStatusCode.SeeOther: + case HttpStatusCode.MultipleChoices: break; default: Method = WebRequestMethod.Get; From 127d94c04e5d35d0c97dc1ea87a378aec386446c Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 6 Jan 2023 19:51:00 +0100 Subject: [PATCH 10/27] Follow suggestions --- .../Common/WebRequestPSCmdlet.Common.cs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) 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 b399d2bccd9..06b59a51064 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 @@ -1279,6 +1279,7 @@ private static bool IsRedirectCode(HttpStatusCode statusCode) } // 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, WebRequestMethod requestMethod) { switch (statusCode) @@ -1338,24 +1339,9 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM } // For selected redirects, GET must be used with the redirected Location. - // See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx if (RequestRequiresForceGet(response.StatusCode, Method)) { - if (PreserveHTTPMethodOnRedirect) - { - switch (response.StatusCode) - { - case HttpStatusCode.Moved: - case HttpStatusCode.Found: - case HttpStatusCode.SeeOther: - case HttpStatusCode.MultipleChoices: - break; - default: - Method = WebRequestMethod.Get; - break; - } - } - else + if (!PreserveHTTPMethodOnRedirect) { Method = WebRequestMethod.Get; } From 575e076556185da0b7209008568b5094b0e74076 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 7 Jan 2023 10:55:11 +0100 Subject: [PATCH 11/27] join if --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 06b59a51064..17496842350 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 @@ -1339,12 +1339,9 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM } // For selected redirects, GET must be used with the redirected Location. - if (RequestRequiresForceGet(response.StatusCode, Method)) + if (RequestRequiresForceGet(response.StatusCode, Method) && !PreserveHTTPMethodOnRedirect) { - if (!PreserveHTTPMethodOnRedirect) - { - Method = WebRequestMethod.Get; - } + Method = WebRequestMethod.Get; } currentUri = new Uri(request.RequestUri, response.Headers.Location); From b7e889a434852fb2d58fb304895c038e580eb137 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 7 Jan 2023 11:07:55 +0100 Subject: [PATCH 12/27] Should Retry remove maximumretrycount add switch --- .../Common/WebRequestPSCmdlet.Common.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 17496842350..bc29e0fc88e 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 @@ -1296,14 +1296,16 @@ private static bool RequestRequiresForceGet(HttpStatusCode statusCode, WebReques } // Returns true if the status code shows a server or client error and MaximumRetryCount > 0 - private bool ShouldRetry(HttpStatusCode code) + private bool ShouldRetry(HttpStatusCode statusCode) { - int intCode = (int)code; - - return - ( - (intCode == 304 || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 0 - ); + switch ((int)statusCode) + { + case 304: + case >= 400 and <= 599: + return true; + default: + return false; + } } internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool keepAuthorization) From c9b5003586cbc168d367b4833cc1a03f1d93170b Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 7 Jan 2023 11:15:41 +0100 Subject: [PATCH 13/27] add static --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bc29e0fc88e..313931d689d 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 @@ -1296,7 +1296,7 @@ private static bool RequestRequiresForceGet(HttpStatusCode statusCode, WebReques } // Returns true if the status code shows a server or client error and MaximumRetryCount > 0 - private bool ShouldRetry(HttpStatusCode statusCode) + private static bool ShouldRetry(HttpStatusCode statusCode) { switch ((int)statusCode) { From 065aa1fbeee67dd80989f76d09ef6d4eade5b038 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sun, 8 Jan 2023 17:45:33 +0100 Subject: [PATCH 14/27] RequestRequiresForceGet WebRequestMethod --> HttpMethod --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 313931d689d..d28cb31fed0 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 @@ -1280,16 +1280,16 @@ private static bool IsRedirectCode(HttpStatusCode statusCode) // 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, WebRequestMethod requestMethod) + private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) { switch (statusCode) { case HttpStatusCode.Moved: case HttpStatusCode.Found: case HttpStatusCode.MultipleChoices: - return requestMethod == WebRequestMethod.Post; + return requestMethod == HttpMethod.Post; case HttpStatusCode.SeeOther: - return requestMethod != WebRequestMethod.Get && requestMethod != WebRequestMethod.Head; + return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head; default: return false; } @@ -1341,7 +1341,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM } // For selected redirects, GET must be used with the redirected Location. - if (RequestRequiresForceGet(response.StatusCode, Method) && !PreserveHTTPMethodOnRedirect) + if (RequestRequiresForceGet(response.StatusCode, req.Method) && !PreserveHTTPMethodOnRedirect) { Method = WebRequestMethod.Get; } From a3438f6ed0785608dccd8faec3d7be3536b4d273 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Mon, 9 Jan 2023 09:59:48 +0100 Subject: [PATCH 15/27] switch expressions --- .../Common/WebRequestPSCmdlet.Common.cs | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) 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 d28cb31fed0..eadb3218c83 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 @@ -1262,51 +1262,34 @@ internal virtual void FillRequestStream(HttpRequestMessage request) } // Returns true if the status code is one of the supported redirection codes. - private static bool IsRedirectCode(HttpStatusCode statusCode) + private static bool IsRedirectCode(HttpStatusCode statusCode) => statusCode switch { - switch (statusCode) - { - case HttpStatusCode.Moved: - case HttpStatusCode.Found: - case HttpStatusCode.SeeOther: - case HttpStatusCode.TemporaryRedirect: - case HttpStatusCode.MultipleChoices: - case HttpStatusCode.PermanentRedirect: - return true; - default: - return false; - } - } + HttpStatusCode.Moved or + HttpStatusCode.Found or + HttpStatusCode.SeeOther or + HttpStatusCode.TemporaryRedirect or + HttpStatusCode.MultipleChoices or + HttpStatusCode.PermanentRedirect => true, + _ => false, + }; // 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) + private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) => statusCode switch { - switch (statusCode) - { - case HttpStatusCode.Moved: - case HttpStatusCode.Found: - case HttpStatusCode.MultipleChoices: - return requestMethod == HttpMethod.Post; - case HttpStatusCode.SeeOther: - return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head; - default: - return false; - } - } + HttpStatusCode.Moved or + HttpStatusCode.Found 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 static bool ShouldRetry(HttpStatusCode statusCode) + private static bool ShouldRetry(HttpStatusCode statusCode) => (int)statusCode switch { - switch ((int)statusCode) - { - case 304: - case >= 400 and <= 599: - return true; - default: - return false; - } - } + 304 or (>= 400 and <= 599) => true, + _ => false, + }; internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool keepAuthorization) { From 3b68566533cb0ca668c3e1d4be004ee8b82ff482 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:40:21 +0100 Subject: [PATCH 16/27] switch expressions alphabetical order --- .../WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 eadb3218c83..b10b9b6423b 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 @@ -1264,12 +1264,12 @@ internal virtual void FillRequestStream(HttpRequestMessage request) // Returns true if the status code is one of the supported redirection codes. private static bool IsRedirectCode(HttpStatusCode statusCode) => statusCode switch { - HttpStatusCode.Moved or HttpStatusCode.Found or - HttpStatusCode.SeeOther or - HttpStatusCode.TemporaryRedirect or + HttpStatusCode.Moved or HttpStatusCode.MultipleChoices or - HttpStatusCode.PermanentRedirect => true, + HttpStatusCode.PermanentRedirect or + HttpStatusCode.SeeOther or + HttpStatusCode.TemporaryRedirect => true, _ => false, }; @@ -1277,8 +1277,8 @@ HttpStatusCode.MultipleChoices or // See https://learn.microsoft.com/en-us/dotnet/api/system.net.httpstatuscode private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) => statusCode switch { - HttpStatusCode.Moved or HttpStatusCode.Found or + HttpStatusCode.Moved or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post, HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head, _ => false, From b7d24f21ddb16331584f8757f4315800343606dc Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Mon, 9 Jan 2023 13:45:44 +0100 Subject: [PATCH 17/27] switch expressions remove last , --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b10b9b6423b..fb543a0bac1 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 @@ -1270,7 +1270,7 @@ HttpStatusCode.MultipleChoices or HttpStatusCode.PermanentRedirect or HttpStatusCode.SeeOther or HttpStatusCode.TemporaryRedirect => true, - _ => false, + _ => false }; // Returns true if the status code is a redirection code and the action requires switching to GET on redirection. @@ -1281,14 +1281,14 @@ HttpStatusCode.Found or HttpStatusCode.Moved or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post, HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head, - _ => false, + _ => false }; // Returns true if the status code shows a server or client error and MaximumRetryCount > 0 private static bool ShouldRetry(HttpStatusCode statusCode) => (int)statusCode switch { 304 or (>= 400 and <= 599) => true, - _ => false, + _ => false }; internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool keepAuthorization) From 9ffef1e22a6421bc96296a04d0c73a037036763c Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:00:41 +0100 Subject: [PATCH 18/27] remove spaces --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a605d0e0e5d..a168aad49ce 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 @@ -1272,7 +1272,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM _cancelToken = new CancellationTokenSource(); response = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult(); - + if (handleRedirect && WebSession.MaximumRedirection is not 0 && IsRedirectCode(response.StatusCode) From ef1872278d80b71d54e7df946ff9731f69c93c8a Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:02:08 +0100 Subject: [PATCH 19/27] handleRedirect = PreserveHTTPMethodOnRedirect --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a168aad49ce..dee83b4fe12 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 @@ -1415,7 +1415,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)) { From 3a29cdc6d94912a3409336d7e7d73f75f88c08f0 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:02:47 +0100 Subject: [PATCH 20/27] remove spaces --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 1 - 1 file changed, 1 deletion(-) 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 dee83b4fe12..baf6c1c1a77 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 @@ -963,7 +963,6 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) } // This indicates GetResponse will handle redirects. - if (handleRedirect || WebSession.MaximumRedirection == 0) { handler.AllowAutoRedirect = false; From c4533ecb419fbc6e8caec2ccf1010b2a940a9d73 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:15:02 +0100 Subject: [PATCH 21/27] HTTP -> Http --- .../utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 baf6c1c1a77..6a157156a08 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 @@ -287,10 +287,10 @@ public virtual string CustomMethod private string _custommethod; /// - /// Gets or sets the PreserveHTTPMethodOnRedirect property. + /// Gets or sets the PreserveHttpMethodOnRedirect property. /// [Parameter] - public virtual SwitchParameter PreserveHTTPMethodOnRedirect { get; set; } + public virtual SwitchParameter PreserveHttpMethodOnRedirect { get; set; } #endregion @@ -1287,7 +1287,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM } // For selected redirects, GET must be used with the redirected Location. - if (RequestRequiresForceGet(response.StatusCode, req.Method) && !PreserveHTTPMethodOnRedirect) + if (RequestRequiresForceGet(response.StatusCode, req.Method) && !PreserveHttpMethodOnRedirect) { Method = WebRequestMethod.Get; } @@ -1414,7 +1414,7 @@ protected override void ProcessRecord() bool keepAuthorizationOnRedirect = PreserveAuthorizationOnRedirect.IsPresent && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization); - bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHTTPMethodOnRedirect; + bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHttpMethodOnRedirect; using (HttpClient client = GetHttpClient(handleRedirect)) { From a9a67de4d1a7d5d2cece42204d977d1618fc5a88 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 13 Jan 2023 23:38:03 +0100 Subject: [PATCH 22/27] add tests --- .../WebCmdlets.Tests.ps1 | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 285be044eb4..e641f6e2395 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -893,7 +893,7 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $response.Error | Should -BeNullOrEmpty $response.Content.Headers."Authorization" | Should -BeExactly "test" } - + It "Validates Invoke-WebRequest with -PreserveAuthorizationOnRedirect respects -MaximumRedirection on redirect: " -TestCases $redirectTests { param($redirectType, $redirectedMethod) $uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3' -Query @{type = $redirectType} @@ -947,12 +947,26 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $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 removed. + # ensure Authorization header has been kept. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $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, $redirectedMethod) + $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. @@ -2641,12 +2655,26 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $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 removed. + # ensure Authorization header has been kept. $response.Content.Headers."Authorization" | Should -BeExactly 'test' # ensure POST was changed to GET for selected redirections and remains as POST for others. $response.Content.Method | Should -Be $redirectedMethod } + It "Validates Invoke-RestMethod -PreserveAuthorizationOnRedirect keeps the authorization header redirects and remains POST when it handles the redirect: " -TestCases $redirectTests { + param($redirectType, $redirectedMethod) + $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} + $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -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. From 495ad8793f656c7332e7eb04e0ed82474bb98a44 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:13:07 +0100 Subject: [PATCH 23/27] add PreserveHttpMethodOnRedirect to ExecuteRedirectRequest --- .../Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index e641f6e2395..4a0ce249b5a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -135,6 +135,9 @@ function ExecuteRedirectRequest { [switch] $PreserveAuthorizationOnRedirect, + [switch] + $PreserveHttpMethodOnRedirect, + [ValidateRange(0, [int]::MaxValue)] [int] $MaximumRedirection @@ -145,13 +148,13 @@ function ExecuteRedirectRequest { $headers = @{"Authorization" = "test"} if ($Cmdlet -eq 'Invoke-WebRequest') { if ($MaximumRedirection -gt 0) { - $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -Method $Method -MaximumRedirection:$MaximumRedirection + $result.Output = Invoke-WebRequest -Uri $uri -Headers $headers -PreserveAuthorizationOnRedirect:$PreserveAuthorizationOnRedirect.IsPresent -PreserveHttpMethodOnRedirect:$PreserveHttpMethodOnRedirect.IsPresent -Method $Method -MaximumRedirection:$MaximumRedirection } 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 { - $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 $result.Content = $result.Output From c2000b77a7cb849a7ad643afe76b08fa730ae7c9 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:51:03 +0100 Subject: [PATCH 24/27] fix tests --- .../WebCmdlets.Tests.ps1 | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 4a0ce249b5a..b9afdb3bac2 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -379,6 +379,13 @@ $redirectTests = @( @{redirectType = 'relative'; redirectedMethod = 'GET'} ) +$redirectTests2 = @( + @{redirectType = 'MultipleChoices'} + @{redirectType = 'Moved'} + @{redirectType = 'Found'} + @{redirectType = 'SeeOther'} +) + Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference @@ -956,8 +963,8 @@ 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, $redirectedMethod) + It "Validates Invoke-WebRequest -PreserveHttpMethodOnRedirect keeps the authorization header redirects and do remains POST when it handles the redirect: " -TestCases $redirectTests2 { + param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Uri $uri -Method 'POST' @@ -2664,10 +2671,10 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $response.Content.Method | Should -Be $redirectedMethod } - It "Validates Invoke-RestMethod -PreserveAuthorizationOnRedirect keeps the authorization header redirects and remains POST when it handles the redirect: " -TestCases $redirectTests { - param($redirectType, $redirectedMethod) + It "Validates Invoke-RestMethod -PreserveHttpMethodOnRedirect keeps the authorization header redirects and remains POST when it handles the redirect: " -TestCases $redirectTests2 { + param($redirectType) $uri = Get-WebListenerUrl -Test 'Redirect' -Query @{type = $redirectType} - $response = ExecuteRedirectRequest -PreserveAuthorizationOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' + $response = ExecuteRedirectRequest -PreserveHttpMethodOnRedirect -Cmdlet 'Invoke-RestMethod' -Uri $uri -Method 'POST' $response.Error | Should -BeNullOrEmpty # ensure user-agent is present (i.e., no false positives ) From 809cb3f2085e5eab047d812d7fc38aff03148db9 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Sat, 14 Jan 2023 00:53:07 +0100 Subject: [PATCH 25/27] reverted --- .../Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index b9afdb3bac2..8f8bc385803 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -379,13 +379,6 @@ $redirectTests = @( @{redirectType = 'relative'; redirectedMethod = 'GET'} ) -$redirectTests2 = @( - @{redirectType = 'MultipleChoices'} - @{redirectType = 'Moved'} - @{redirectType = 'Found'} - @{redirectType = 'SeeOther'} -) - Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference @@ -963,7 +956,7 @@ 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 $redirectTests2 { + 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' @@ -2671,7 +2664,7 @@ 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 $redirectTests2 { + 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' From c8aae98c7fb5617fba853ad19509d48c15699683 Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Fri, 10 Feb 2023 10:46:08 +0100 Subject: [PATCH 26/27] merge --- .../Common/WebRequestPSCmdlet.Common.cs | 938 +++++++++--------- 1 file changed, 490 insertions(+), 448 deletions(-) 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 783481147c6..f8a1b1063e7 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 @@ -16,6 +16,8 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -86,8 +88,47 @@ public enum WebSslProtocol /// /// Base class for Invoke-RestMethod and Invoke-WebRequest commands. /// - public abstract partial class WebRequestPSCmdlet : PSCmdlet + public abstract class WebRequestPSCmdlet : PSCmdlet { + #region Fields + + /// + /// Cancellation token source. + /// + internal CancellationTokenSource _cancelToken = null; + + /// + /// Automatically follow Rel Links. + /// + internal bool _followRelLink = false; + + /// + /// Maximum number of Rel Links to follow. + /// + internal int _maximumFollowRelLink = int.MaxValue; + + /// + /// Parse Rel Links. + /// + internal bool _parseRelLink = false; + + /// + /// Automatically follow Rel Links. + /// + internal Dictionary _relationLink = null; + + /// + /// The current size of the local file being resumed. + /// + private long _resumeFileSize = 0; + + /// + /// The remote endpoint returned a 206 status code indicating successful resume. + /// + private bool _resumeSuccess = false; + + #endregion Fields + #region Virtual Properties #region URI @@ -268,7 +309,7 @@ public abstract partial class WebRequestPSCmdlet : PSCmdlet /// This property overrides compatibility with web requests on Windows. /// On FullCLR (WebRequest), authorization headers are stripped during redirect. /// CoreCLR (HTTPClient) does not have this behavior so web requests that work on - /// PowerShell/FullCLR can fail with PowerShell/CoreCLR. To provide compatibility, + /// PowerShell/FullCLR can fail with PowerShell/CoreCLR. To provide compatibility, /// we'll detect requests with an Authorization header and automatically strip /// the header when the first redirect occurs. This switch turns off this logic for /// edge cases where the authorization header needs to be preserved across redirects. @@ -429,6 +470,213 @@ public virtual string CustomMethod #endregion Virtual Properties + #region Helper Properties + + internal string QualifiedOutFile => QualifyFilePath(OutFile); + + internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; + + /// + /// Determines whether writing to a file should Resume and append rather than overwrite. + /// + internal bool ShouldResume => Resume.IsPresent && _resumeSuccess; + + internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutFile); + + internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile || PassThru; + + #endregion Helper Properties + + #region Abstract Methods + + /// + /// Read the supplied WebResponse object and push the resulting output into the pipeline. + /// + /// Instance of a WebResponse object to be processed. + internal abstract void ProcessResponse(HttpResponseMessage response); + + #endregion Abstract Methods + + #region Overrides + + /// + /// The main execution method for cmdlets derived from WebRequestPSCmdlet. + /// + protected override void ProcessRecord() + { + try + { + // Set cmdlet context for write progress + ValidateParameters(); + PrepareSession(); + + // If the request contains an authorization header and PreserveAuthorizationOnRedirect is not set, + // it needs to be stripped on the first redirect. + bool keepAuthorizationOnRedirect = PreserveAuthorizationOnRedirect.IsPresent + && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization); + + bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHttpMethodOnRedirect; + + using (HttpClient client = GetHttpClient(handleRedirect)) + { + int followedRelLink = 0; + Uri uri = Uri; + do + { + if (followedRelLink > 0) + { + string linkVerboseMsg = string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.FollowingRelLinkVerboseMsg, + uri.AbsoluteUri); + + WriteVerbose(linkVerboseMsg); + } + + using (HttpRequestMessage request = GetRequest(uri)) + { + FillRequestStream(request); + try + { + long requestContentLength = request.Content is null ? 0 : request.Content.Headers.ContentLength.Value; + + string reqVerboseMsg = string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.WebMethodInvocationVerboseMsg, + request.Version, + request.Method, + requestContentLength); + + WriteVerbose(reqVerboseMsg); + + using HttpResponseMessage response = GetResponse(client, request, handleRedirect); + + string contentType = ContentHelper.GetContentType(response); + string respVerboseMsg = string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.WebResponseVerboseMsg, + response.Content.Headers.ContentLength, + contentType); + + WriteVerbose(respVerboseMsg); + + bool _isSuccess = response.IsSuccessStatusCode; + + // Check if the Resume range was not satisfiable because the file already completed downloading. + // This happens when the local file is the same size as the remote file. + if (Resume.IsPresent + && response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable + && response.Content.Headers.ContentRange.HasLength + && response.Content.Headers.ContentRange.Length == _resumeFileSize) + { + _isSuccess = true; + WriteVerbose(string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.OutFileWritingSkipped, + OutFile)); + + // Disable writing to the OutFile. + OutFile = null; + } + + if (ShouldCheckHttpStatus && !_isSuccess) + { + string message = string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.ResponseStatusCodeFailure, + (int)response.StatusCode, + response.ReasonPhrase); + + HttpResponseException httpEx = new(message, response); + ErrorRecord er = new(httpEx, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); + string detailMsg = string.Empty; + StreamReader reader = null; + try + { + reader = new StreamReader(StreamHelper.GetResponseStream(response)); + detailMsg = FormatErrorMessage(reader.ReadToEnd(), contentType); + } + catch + { + // Catch all + } + finally + { + reader?.Dispose(); + } + + if (!string.IsNullOrEmpty(detailMsg)) + { + er.ErrorDetails = new ErrorDetails(detailMsg); + } + + ThrowTerminatingError(er); + } + + if (_parseRelLink || _followRelLink) + { + ParseLinkHeader(response, uri); + } + + ProcessResponse(response); + UpdateSession(response); + + // If we hit our maximum redirection count, generate an error. + // Errors with redirection counts of greater than 0 are handled automatically by .NET, but are + // impossible to detect programmatically when we hit this limit. By handling this ourselves + // (and still writing out the result), users can debug actual HTTP redirect problems. + if (WebSession.MaximumRedirection == 0 && IsRedirectCode(response.StatusCode)) + { + ErrorRecord er = new(new InvalidOperationException(), "MaximumRedirectExceeded", ErrorCategory.InvalidOperation, request); + er.ErrorDetails = new ErrorDetails(WebCmdletStrings.MaximumRedirectionCountExceeded); + WriteError(er); + } + } + catch (HttpRequestException ex) + { + ErrorRecord er = new(ex, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); + if (ex.InnerException is not null) + { + er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); + } + + ThrowTerminatingError(er); + } + + if (_followRelLink) + { + if (!_relationLink.ContainsKey("next")) + { + return; + } + + uri = new Uri(_relationLink["next"]); + followedRelLink++; + } + } + } + while (_followRelLink && (followedRelLink < _maximumFollowRelLink)); + } + } + catch (CryptographicException ex) + { + ErrorRecord er = new(ex, "WebCmdletCertificateException", ErrorCategory.SecurityError, null); + ThrowTerminatingError(er); + } + catch (NotSupportedException ex) + { + ErrorRecord er = new(ex, "WebCmdletIEDomNotSupportedException", ErrorCategory.NotImplemented, null); + ThrowTerminatingError(er); + } + } + + /// + /// To implement ^C. + /// + protected override void StopProcessing() => _cancelToken?.Cancel(); + + #endregion Overrides + #region Virtual Methods internal virtual void ValidateParameters() @@ -580,24 +828,24 @@ internal virtual void ValidateParameters() internal virtual void PrepareSession() { - // make sure we have a valid WebRequestSession object to work with + // Make sure we have a valid WebRequestSession object to work with WebSession ??= new WebRequestSession(); if (SessionVariable is not null) { - // save the session back to the PS environment if requested + // Save the session back to the PS environment if requested PSVariableIntrinsics vi = SessionState.PSVariable; vi.Set(SessionVariable, WebSession); } - // handle credentials + // Handle credentials if (Credential is not null && Authentication == WebAuthenticationType.None) { - // get the relevant NetworkCredential + // Get the relevant NetworkCredential NetworkCredential netCred = Credential.GetNetworkCredential(); WebSession.Credentials = netCred; - // supplying a credential overrides the UseDefaultCredentials setting + // Supplying a credential overrides the UseDefaultCredentials setting WebSession.UseDefaultCredentials = false; } else if ((Credential is not null || Token is not null) && Authentication != WebAuthenticationType.None) @@ -633,10 +881,10 @@ internal virtual void PrepareSession() WebSession.AddCertificate(Certificate); } - // handle the user agent + // Handle the user agent if (UserAgent is not null) { - // store the UserAgent string + // Store the UserAgent string WebSession.UserAgent = UserAgent; } @@ -663,7 +911,7 @@ internal virtual void PrepareSession() WebSession.MaximumRedirection = MaximumRedirection; } - // store the other supplied headers + // Store the other supplied headers if (Headers is not null) { foreach (string key in Headers.Keys) @@ -689,252 +937,22 @@ internal virtual void PrepareSession() } } - #endregion Virtual Methods + internal virtual HttpClient GetHttpClient(bool handleRedirect) + { + HttpClientHandler handler = new(); + handler.CookieContainer = WebSession.Cookies; + handler.AutomaticDecompression = DecompressionMethods.All; - #region Helper Properties - - internal string QualifiedOutFile => QualifyFilePath(OutFile); - - internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutFile); - - internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile || PassThru; - - internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; - - /// - /// Determines whether writing to a file should Resume and append rather than overwrite. - /// - internal bool ShouldResume => Resume.IsPresent && _resumeSuccess; - - #endregion Helper Properties - - #region Helper Methods - private Uri PrepareUri(Uri uri) - { - uri = CheckProtocol(uri); - - // Before creating the web request, - // preprocess Body if content is a dictionary and method is GET (set as query) - LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary); - if (bodyAsDictionary is not null && (Method == WebRequestMethod.Default || Method == WebRequestMethod.Get || CustomMethod == "GET")) - { - UriBuilder uriBuilder = new(uri); - if (uriBuilder.Query is not null && uriBuilder.Query.Length > 1) - { - uriBuilder.Query = string.Concat(uriBuilder.Query.AsSpan(1), "&", FormatDictionary(bodyAsDictionary)); - } - else - { - uriBuilder.Query = FormatDictionary(bodyAsDictionary); - } - - uri = uriBuilder.Uri; - // set body to null to prevent later FillRequestStream - Body = null; - } - - return uri; - } - - private static Uri CheckProtocol(Uri uri) - { - ArgumentNullException.ThrowIfNull(uri); - - if (!uri.IsAbsoluteUri) - { - uri = new Uri("http://" + uri.OriginalString); - } - - return uri; - } - - private string QualifyFilePath(string path) - { - string resolvedFilePath = PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true); - return resolvedFilePath; - } - - private static string FormatDictionary(IDictionary content) - { - ArgumentNullException.ThrowIfNull(content); - - StringBuilder bodyBuilder = new(); - foreach (string key in content.Keys) - { - if (bodyBuilder.Length > 0) - { - bodyBuilder.Append('&'); - } - - object value = content[key]; - - // URLEncode the key and value - string encodedKey = WebUtility.UrlEncode(key); - string encodedValue = string.Empty; - if (value is not null) - { - encodedValue = WebUtility.UrlEncode(value.ToString()); - } - - bodyBuilder.Append($"{encodedKey}={encodedValue}"); - } - - return bodyBuilder.ToString(); - } - - private ErrorRecord GetValidationError(string msg, string errorId) - { - var ex = new ValidationMetadataException(msg); - var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); - return error; - } - - private ErrorRecord GetValidationError(string msg, string errorId, params object[] args) - { - msg = string.Format(CultureInfo.InvariantCulture, msg, args); - var ex = new ValidationMetadataException(msg); - var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); - return error; - } - - private string GetBasicAuthorizationHeader() - { - var password = new NetworkCredential(null, Credential.Password).Password; - string unencoded = string.Create(CultureInfo.InvariantCulture, $"{Credential.UserName}:{password}"); - byte[] bytes = Encoding.UTF8.GetBytes(unencoded); - return string.Create(CultureInfo.InvariantCulture, $"Basic {Convert.ToBase64String(bytes)}"); - } - - private string GetBearerAuthorizationHeader() - { - return string.Create(CultureInfo.InvariantCulture, $"Bearer {new NetworkCredential(string.Empty, Token).Password}"); - } - - private void ProcessAuthentication() - { - if (Authentication == WebAuthenticationType.Basic) - { - WebSession.Headers["Authorization"] = GetBasicAuthorizationHeader(); - } - else if (Authentication == WebAuthenticationType.Bearer || Authentication == WebAuthenticationType.OAuth) - { - WebSession.Headers["Authorization"] = GetBearerAuthorizationHeader(); - } - else - { - Diagnostics.Assert(false, string.Create(CultureInfo.InvariantCulture, $"Unrecognized Authentication value: {Authentication}")); - } - } - - #endregion Helper Methods - } - - // TODO: Merge Partials - - /// - /// Exception class for webcmdlets to enable returning HTTP error response. - /// - public sealed class HttpResponseException : HttpRequestException - { - /// - /// Initializes a new instance of the class. - /// - /// Message for the exception. - /// Response from the HTTP server. - public HttpResponseException(string message, HttpResponseMessage response) : base(message, inner: null, response.StatusCode) - { - Response = response; - } - - /// - /// HTTP error response. - /// - public HttpResponseMessage Response { get; } - } - - /// - /// Base class for Invoke-RestMethod and Invoke-WebRequest commands. - /// - public abstract partial class WebRequestPSCmdlet : PSCmdlet - { - #region Abstract Methods - - /// - /// Read the supplied WebResponse object and push the resulting output into the pipeline. - /// - /// Instance of a WebResponse object to be processed. - internal abstract void ProcessResponse(HttpResponseMessage response); - - #endregion Abstract Methods - - /// - /// Cancellation token source. - /// - internal CancellationTokenSource _cancelToken = null; - - /// - /// Parse Rel Links. - /// - internal bool _parseRelLink = false; - - /// - /// Automatically follow Rel Links. - /// - internal bool _followRelLink = false; - - /// - /// Automatically follow Rel Links. - /// - internal Dictionary _relationLink = null; - - /// - /// Maximum number of Rel Links to follow. - /// - internal int _maximumFollowRelLink = int.MaxValue; - - /// - /// The remote endpoint returned a 206 status code indicating successful resume. - /// - private bool _resumeSuccess = false; - - /// - /// The current size of the local file being resumed. - /// - private long _resumeFileSize = 0; - - private static HttpMethod GetHttpMethod(WebRequestMethod method) => method switch - { - WebRequestMethod.Default or WebRequestMethod.Get => HttpMethod.Get, - WebRequestMethod.Delete => HttpMethod.Delete, - WebRequestMethod.Head => HttpMethod.Head, - WebRequestMethod.Patch => HttpMethod.Patch, - WebRequestMethod.Post => HttpMethod.Post, - WebRequestMethod.Put => HttpMethod.Put, - WebRequestMethod.Options => HttpMethod.Options, - WebRequestMethod.Trace => HttpMethod.Trace, - _ => new HttpMethod(method.ToString().ToUpperInvariant()) - }; - - #region Virtual Methods - - // NOTE: Only pass true for handleRedirect if the original request has an authorization header - // and PreserveAuthorizationOnRedirect is NOT set. - internal virtual HttpClient GetHttpClient(bool handleRedirect) - { - HttpClientHandler handler = new(); - handler.CookieContainer = WebSession.Cookies; - handler.AutomaticDecompression = DecompressionMethods.All; - - // set the credentials used by this request - if (WebSession.UseDefaultCredentials) - { - // the UseDefaultCredentials flag overrides other supplied credentials - handler.UseDefaultCredentials = true; - } - else if (WebSession.Credentials is not null) - { - handler.Credentials = WebSession.Credentials; - } + // Set the credentials used by this request + if (WebSession.UseDefaultCredentials) + { + // The UseDefaultCredentials flag overrides other supplied credentials + handler.UseDefaultCredentials = true; + } + else if (WebSession.Credentials is not null) + { + handler.Credentials = WebSession.Credentials; + } if (NoProxy) { @@ -981,7 +999,7 @@ internal virtual HttpRequestMessage GetRequest(Uri uri) Uri requestUri = PrepareUri(uri); HttpMethod httpMethod = string.IsNullOrEmpty(CustomMethod) ? GetHttpMethod(Method) : new HttpMethod(CustomMethod); - // create the base WebRequest object + // Create the base WebRequest object var request = new HttpRequestMessage(httpMethod, requestUri); if (HttpVersion is not null) @@ -989,7 +1007,7 @@ internal virtual HttpRequestMessage GetRequest(Uri uri) request.Version = HttpVersion; } - // pull in session data + // Pull in session data if (WebSession.Headers.Count > 0) { WebSession.ContentHeaders.Clear(); @@ -1076,13 +1094,11 @@ internal virtual void FillRequestStream(HttpRequestMessage request) { ArgumentNullException.ThrowIfNull(request); - // set the content type + // Set the request content type if (ContentType is not null) { WebSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = ContentType; - // request } - // ContentType is null else if (request.Method == HttpMethod.Post) { // Win8:545310 Invoke-WebRequest does not properly set MIME type for POST @@ -1194,36 +1210,6 @@ internal virtual void FillRequestStream(HttpRequestMessage request) } } - // Returns true if the status code is one of the supported redirection codes. - private static bool IsRedirectCode(HttpStatusCode statusCode) => statusCode switch - { - 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 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 - { - 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 static bool ShouldRetry(HttpStatusCode statusCode) => (int)statusCode switch - { - 304 or (>= 400 and <= 599) => true, - _ => false - }; - internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool handleRedirect) { ArgumentNullException.ThrowIfNull(client); @@ -1266,6 +1252,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // Continue to handle redirection using HttpRequestMessage redirectRequest = GetRequest(currentUri); + response.Dispose(); response = GetResponse(client, redirectRequest, handleRedirect); } @@ -1300,7 +1287,8 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM WriteVerbose(reqVerboseMsg); - return GetResponse(client, requestWithoutRange, handleRedirect); + response.Dispose(); + response = GetResponse(client, requestWithoutRange, handleRedirect); } } @@ -1361,190 +1349,125 @@ internal virtual void UpdateSession(HttpResponseMessage response) #endregion Virtual Methods - #region Overrides - - /// - /// The main execution method for cmdlets derived from WebRequestPSCmdlet. - /// - protected override void ProcessRecord() + #region Helper Methods + private Uri PrepareUri(Uri uri) { - try - { - // Set cmdlet context for write progress - ValidateParameters(); - PrepareSession(); - - // If the request contains an authorization header and PreserveAuthorizationOnRedirect is not set, - // it needs to be stripped on the first redirect. - bool keepAuthorizationOnRedirect = PreserveAuthorizationOnRedirect.IsPresent - && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization); - - bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHttpMethodOnRedirect; + uri = CheckProtocol(uri); - using (HttpClient client = GetHttpClient(handleRedirect)) + // Before creating the web request, + // preprocess Body if content is a dictionary and method is GET (set as query) + LanguagePrimitives.TryConvertTo(Body, out IDictionary bodyAsDictionary); + if (bodyAsDictionary is not null && (Method == WebRequestMethod.Default || Method == WebRequestMethod.Get || CustomMethod == "GET")) + { + UriBuilder uriBuilder = new(uri); + if (uriBuilder.Query is not null && uriBuilder.Query.Length > 1) { - int followedRelLink = 0; - Uri uri = Uri; - do - { - if (followedRelLink > 0) - { - string linkVerboseMsg = string.Format( - CultureInfo.CurrentCulture, - WebCmdletStrings.FollowingRelLinkVerboseMsg, - uri.AbsoluteUri); - - WriteVerbose(linkVerboseMsg); - } - - using (HttpRequestMessage request = GetRequest(uri)) - { - FillRequestStream(request); - try - { - long requestContentLength = request.Content is null ? 0 : request.Content.Headers.ContentLength.Value; - - string reqVerboseMsg = string.Format( - CultureInfo.CurrentCulture, - WebCmdletStrings.WebMethodInvocationVerboseMsg, - request.Version, - request.Method, - requestContentLength); + uriBuilder.Query = string.Concat(uriBuilder.Query.AsSpan(1), "&", FormatDictionary(bodyAsDictionary)); + } + else + { + uriBuilder.Query = FormatDictionary(bodyAsDictionary); + } - WriteVerbose(reqVerboseMsg); + uri = uriBuilder.Uri; - HttpResponseMessage response = GetResponse(client, request, handleRedirect); + // Set body to null to prevent later FillRequestStream + Body = null; + } - string contentType = ContentHelper.GetContentType(response); - string respVerboseMsg = string.Format( - CultureInfo.CurrentCulture, - WebCmdletStrings.WebResponseVerboseMsg, - response.Content.Headers.ContentLength, - contentType); + return uri; + } - WriteVerbose(respVerboseMsg); + private static Uri CheckProtocol(Uri uri) + { + ArgumentNullException.ThrowIfNull(uri); - bool _isSuccess = response.IsSuccessStatusCode; + if (!uri.IsAbsoluteUri) + { + uri = new Uri("http://" + uri.OriginalString); + } - // Check if the Resume range was not satisfiable because the file already completed downloading. - // This happens when the local file is the same size as the remote file. - if (Resume.IsPresent - && response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable - && response.Content.Headers.ContentRange.HasLength - && response.Content.Headers.ContentRange.Length == _resumeFileSize) - { - _isSuccess = true; - WriteVerbose(string.Format( - CultureInfo.CurrentCulture, - WebCmdletStrings.OutFileWritingSkipped, - OutFile)); + return uri; + } - // Disable writing to the OutFile. - OutFile = null; - } + private string QualifyFilePath(string path) + { + string resolvedFilePath = PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true); + return resolvedFilePath; + } - if (ShouldCheckHttpStatus && !_isSuccess) - { - string message = string.Format( - CultureInfo.CurrentCulture, - WebCmdletStrings.ResponseStatusCodeFailure, - (int)response.StatusCode, - response.ReasonPhrase); + private static string FormatDictionary(IDictionary content) + { + ArgumentNullException.ThrowIfNull(content); - HttpResponseException httpEx = new(message, response); - ErrorRecord er = new(httpEx, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); - string detailMsg = string.Empty; - StreamReader reader = null; - try - { - reader = new(StreamHelper.GetResponseStream(response)); + StringBuilder bodyBuilder = new(); + foreach (string key in content.Keys) + { + if (bodyBuilder.Length > 0) + { + bodyBuilder.Append('&'); + } - // Remove HTML tags making it easier to read - detailMsg = System.Text.RegularExpressions.Regex.Replace(reader.ReadToEnd(), "<[^>]*>", string.Empty); - } - catch - { - // Catch all - } - finally - { - reader?.Dispose(); - } + object value = content[key]; - if (!string.IsNullOrEmpty(detailMsg)) - { - er.ErrorDetails = new ErrorDetails(detailMsg); - } + // URLEncode the key and value + string encodedKey = WebUtility.UrlEncode(key); + string encodedValue = string.Empty; + if (value is not null) + { + encodedValue = WebUtility.UrlEncode(value.ToString()); + } - ThrowTerminatingError(er); - } + bodyBuilder.Append($"{encodedKey}={encodedValue}"); + } - if (_parseRelLink || _followRelLink) - { - ParseLinkHeader(response, uri); - } + return bodyBuilder.ToString(); + } - ProcessResponse(response); - UpdateSession(response); + private ErrorRecord GetValidationError(string msg, string errorId) + { + var ex = new ValidationMetadataException(msg); + var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); + return error; + } - // If we hit our maximum redirection count, generate an error. - // Errors with redirection counts of greater than 0 are handled automatically by .NET, but are - // impossible to detect programmatically when we hit this limit. By handling this ourselves - // (and still writing out the result), users can debug actual HTTP redirect problems. - if (WebSession.MaximumRedirection == 0 && IsRedirectCode(response.StatusCode)) // Indicate "HttpClientHandler.AllowAutoRedirect is false" - { - ErrorRecord er = new(new InvalidOperationException(), "MaximumRedirectExceeded", ErrorCategory.InvalidOperation, request); - er.ErrorDetails = new ErrorDetails(WebCmdletStrings.MaximumRedirectionCountExceeded); - WriteError(er); - } - } - catch (HttpRequestException ex) - { - ErrorRecord er = new(ex, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); - if (ex.InnerException is not null) - { - er.ErrorDetails = new ErrorDetails(ex.InnerException.Message); - } + private ErrorRecord GetValidationError(string msg, string errorId, params object[] args) + { + msg = string.Format(CultureInfo.InvariantCulture, msg, args); + var ex = new ValidationMetadataException(msg); + var error = new ErrorRecord(ex, errorId, ErrorCategory.InvalidArgument, this); + return error; + } - ThrowTerminatingError(er); - } + private string GetBasicAuthorizationHeader() + { + var password = new NetworkCredential(null, Credential.Password).Password; + string unencoded = string.Create(CultureInfo.InvariantCulture, $"{Credential.UserName}:{password}"); + byte[] bytes = Encoding.UTF8.GetBytes(unencoded); + return string.Create(CultureInfo.InvariantCulture, $"Basic {Convert.ToBase64String(bytes)}"); + } - if (_followRelLink) - { - if (!_relationLink.ContainsKey("next")) - { - return; - } + private string GetBearerAuthorizationHeader() + { + return string.Create(CultureInfo.InvariantCulture, $"Bearer {new NetworkCredential(string.Empty, Token).Password}"); + } - uri = new Uri(_relationLink["next"]); - followedRelLink++; - } - } - } - while (_followRelLink && (followedRelLink < _maximumFollowRelLink)); - } + private void ProcessAuthentication() + { + if (Authentication == WebAuthenticationType.Basic) + { + WebSession.Headers["Authorization"] = GetBasicAuthorizationHeader(); } - catch (CryptographicException ex) + else if (Authentication == WebAuthenticationType.Bearer || Authentication == WebAuthenticationType.OAuth) { - ErrorRecord er = new(ex, "WebCmdletCertificateException", ErrorCategory.SecurityError, null); - ThrowTerminatingError(er); + WebSession.Headers["Authorization"] = GetBearerAuthorizationHeader(); } - catch (NotSupportedException ex) + else { - ErrorRecord er = new(ex, "WebCmdletIEDomNotSupportedException", ErrorCategory.NotImplemented, null); - ThrowTerminatingError(er); + Diagnostics.Assert(false, string.Create(CultureInfo.InvariantCulture, $"Unrecognized Authentication value: {Authentication}")); } } - /// - /// To implement ^C. - /// - protected override void StopProcessing() => _cancelToken?.Cancel(); - - #endregion Overrides - - #region Helper Methods - /// /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream. /// @@ -1719,7 +1642,7 @@ internal void ParseLinkHeader(HttpResponseMessage response, System.Uri requestUr /// /// The Field Name to use. /// The Field Value to use. - /// The > to update. + /// The to update. /// If true, collection types in will be enumerated. If false, collections will be treated as single value. private void AddMultipartContent(object fieldName, object fieldValue, MultipartFormDataContent formData, bool enumerate) { @@ -1810,11 +1733,130 @@ private static StreamContent GetMultipartStreamContent(object fieldName, Stream private static StreamContent GetMultipartFileContent(object fieldName, FileInfo file) { var 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 + "\""; return result; } + + private static string FormatErrorMessage(string error, string contentType) + { + string formattedError = null; + + try + { + if (ContentHelper.IsXml(contentType)) + { + XmlDocument doc = new(); + doc.LoadXml(error); + + XmlWriterSettings settings = new XmlWriterSettings { + Indent = true, + NewLineOnAttributes = true, + OmitXmlDeclaration = true + }; + + if (doc.FirstChild is XmlDeclaration) + { + XmlDeclaration decl = doc.FirstChild as XmlDeclaration; + settings.Encoding = Encoding.GetEncoding(decl.Encoding); + } + + StringBuilder stringBuilder = new(); + using XmlWriter xmlWriter = XmlWriter.Create(stringBuilder, settings); + doc.Save(xmlWriter); + string xmlString = stringBuilder.ToString(); + + formattedError = Environment.NewLine + xmlString; + } + else if (ContentHelper.IsJson(contentType)) + { + JsonNode jsonNode = JsonNode.Parse(error); + JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true }; + string jsonString = jsonNode.ToJsonString(options); + + formattedError = Environment.NewLine + jsonString; + } + } + catch + { + // Ignore errors + } + + if (string.IsNullOrEmpty(formattedError)) + { + // Remove HTML tags making it easier to read + formattedError = System.Text.RegularExpressions.Regex.Replace(error, "<[^>]*>", string.Empty); + } + + return formattedError; + } + + // Returns true if the status code is one of the supported redirection codes. + private static bool IsRedirectCode(HttpStatusCode statusCode) => statusCode switch + { + 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 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 + { + 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 static bool ShouldRetry(HttpStatusCode statusCode) => (int)statusCode switch + { + 304 or (>= 400 and <= 599) => true, + _ => false + }; + + private static HttpMethod GetHttpMethod(WebRequestMethod method) => method switch + { + WebRequestMethod.Default or WebRequestMethod.Get => HttpMethod.Get, + WebRequestMethod.Delete => HttpMethod.Delete, + WebRequestMethod.Head => HttpMethod.Head, + WebRequestMethod.Patch => HttpMethod.Patch, + WebRequestMethod.Post => HttpMethod.Post, + WebRequestMethod.Put => HttpMethod.Put, + WebRequestMethod.Options => HttpMethod.Options, + WebRequestMethod.Trace => HttpMethod.Trace, + _ => new HttpMethod(method.ToString().ToUpperInvariant()) + }; + #endregion Helper Methods } + + /// + /// Exception class for webcmdlets to enable returning HTTP error response. + /// + public sealed class HttpResponseException : HttpRequestException + { + /// + /// Initializes a new instance of the class. + /// + /// Message for the exception. + /// Response from the HTTP server. + public HttpResponseException(string message, HttpResponseMessage response) : base(message, inner: null, response.StatusCode) + { + Response = response; + } + + /// + /// HTTP error response. + /// + public HttpResponseMessage Response { get; } + } } From c91c8f3ee1fdf1e45b95bcd887faf72fa29cddbf Mon Sep 17 00:00:00 2001 From: CarloToso <105941898+CarloToso@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:06:29 +0100 Subject: [PATCH 27/27] moved or --- .../Common/WebRequestPSCmdlet.Common.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 61e331e5ad7..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 @@ -1780,12 +1780,12 @@ 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 statusCode) => statusCode switch { - HttpStatusCode.Found or - HttpStatusCode.Moved or - HttpStatusCode.MultipleChoices or - HttpStatusCode.PermanentRedirect or - HttpStatusCode.SeeOther or - HttpStatusCode.TemporaryRedirect => true, + HttpStatusCode.Found + or HttpStatusCode.Moved + or HttpStatusCode.MultipleChoices + or HttpStatusCode.PermanentRedirect + or HttpStatusCode.SeeOther + or HttpStatusCode.TemporaryRedirect => true, _ => false }; @@ -1793,9 +1793,9 @@ HttpStatusCode.SeeOther or // See https://learn.microsoft.com/en-us/dotnet/api/system.net.httpstatuscode private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod) => statusCode switch { - HttpStatusCode.Found or - HttpStatusCode.Moved or - HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post, + HttpStatusCode.Found + or HttpStatusCode.Moved + or HttpStatusCode.MultipleChoices => requestMethod == HttpMethod.Post, HttpStatusCode.SeeOther => requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head, _ => false };