From 9e341148f40bd23ac71d90174a80ca3186f8f6b3 Mon Sep 17 00:00:00 2001 From: stevenebutler Date: Wed, 8 Mar 2023 03:42:08 +1000 Subject: [PATCH] Support HTTP persistent connections in Web Cmdlets (#19249) --- .../Common/WebRequestPSCmdlet.Common.cs | 144 ++++++------ .../utility/WebCmdlet/CoreCLR/WebProxy.cs | 19 +- .../utility/WebCmdlet/WebRequestSession.cs | 221 ++++++++++++++++-- .../resources/WebCmdletStrings.resx | 3 + .../WebCmdlets.Tests.ps1 | 142 +++++++++++ 5 files changed, 444 insertions(+), 85 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 de26f49c75..acf8135f73 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 @@ -88,7 +88,7 @@ namespace Microsoft.PowerShell.Commands /// /// Base class for Invoke-RestMethod and Invoke-WebRequest commands. /// - public abstract class WebRequestPSCmdlet : PSCmdlet + public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable { #region Fields @@ -132,6 +132,11 @@ namespace Microsoft.PowerShell.Commands /// private bool _resumeSuccess = false; + /// + /// True if the Dispose() method has already been called to cleanup Disposable fields. + /// + private bool _disposed = false; + #endregion Fields #region Virtual Properties @@ -522,7 +527,7 @@ namespace Microsoft.PowerShell.Commands bool handleRedirect = keepAuthorizationOnRedirect || AllowInsecureRedirect || PreserveHttpMethodOnRedirect; - using HttpClient client = GetHttpClient(handleRedirect); + HttpClient client = GetHttpClient(handleRedirect); int followedRelLink = 0; Uri uri = Uri; @@ -639,7 +644,7 @@ namespace Microsoft.PowerShell.Commands // 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)) + if (_maximumRedirection == 0 && IsRedirectCode(response.StatusCode)) { ErrorRecord er = new(new InvalidOperationException(), "MaximumRedirectExceeded", ErrorCategory.InvalidOperation, request); er.ErrorDetails = new ErrorDetails(WebCmdletStrings.MaximumRedirectionCountExceeded); @@ -688,6 +693,33 @@ namespace Microsoft.PowerShell.Commands /// protected override void StopProcessing() => _cancelToken?.Cancel(); + /// + /// Disposes the associated WebSession if it is not being used as part of a persistent session. + /// + /// True when called from Dispose() and false when called from finalizer. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing && !IsPersistentSession()) + { + WebSession?.Dispose(); + WebSession = null; + } + + _disposed = true; + } + } + + /// + /// Disposes the associated WebSession if it is not being used as part of a persistent session. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion Overrides #region Virtual Methods @@ -900,20 +932,41 @@ namespace Microsoft.PowerShell.Commands WebSession.UserAgent = UserAgent; } - if (Proxy is not null) + // Proxy and NoProxy parameters are mutually exclusive. + // If NoProxy is provided, WebSession will turn off the proxy + // and if Proxy is provided NoProxy will be turned off. + if (NoProxy.IsPresent) { - WebProxy webProxy = new(Proxy); - webProxy.BypassProxyOnLocal = false; - if (ProxyCredential is not null) + WebSession.NoProxy = true; + } + else + { + if (Proxy is not null) { - webProxy.Credentials = ProxyCredential.GetNetworkCredential(); - } - else - { - webProxy.UseDefaultCredentials = ProxyUseDefaultCredentials; - } + WebProxy webProxy = new(Proxy); + webProxy.BypassProxyOnLocal = false; + if (ProxyCredential is not null) + { + webProxy.Credentials = ProxyCredential.GetNetworkCredential(); + } + else + { + webProxy.UseDefaultCredentials = ProxyUseDefaultCredentials; + } - WebSession.Proxy = webProxy; + // We don't want to update the WebSession unless the proxies are different + // as that will require us to create a new HttpClientHandler and lose connection + // persistence. + if (!webProxy.Equals(WebSession.Proxy)) + { + WebSession.Proxy = webProxy; + } + } + } + + if (MyInvocation.BoundParameters.ContainsKey(nameof(SslProtocol))) + { + WebSession.SslProtocol = SslProtocol; } if (MaximumRedirection > -1) @@ -921,6 +974,8 @@ namespace Microsoft.PowerShell.Commands WebSession.MaximumRedirection = MaximumRedirection; } + WebSession.SkipCertificateCheck = SkipCertificateCheck.IsPresent; + // Store the other supplied headers if (Headers is not null) { @@ -945,63 +1000,20 @@ namespace Microsoft.PowerShell.Commands // Only set retry interval if retry count is set. WebSession.RetryIntervalInSeconds = RetryIntervalSec; } + + WebSession.TimeoutSec = TimeoutSec; } internal virtual HttpClient GetHttpClient(bool handleRedirect) { - HttpClientHandler handler = new(); - handler.CookieContainer = WebSession.Cookies; - handler.AutomaticDecompression = DecompressionMethods.All; + HttpClient client = WebSession.GetHttpClient(handleRedirect, out bool clientWasReset); - // Set the credentials used by this request - if (WebSession.UseDefaultCredentials) + if (clientWasReset) { - // The UseDefaultCredentials flag overrides other supplied credentials - handler.UseDefaultCredentials = true; - } - else if (WebSession.Credentials is not null) - { - handler.Credentials = WebSession.Credentials; + WriteVerbose(WebCmdletStrings.WebSessionConnectionRecreated); } - if (NoProxy) - { - handler.UseProxy = false; - } - else if (WebSession.Proxy is not null) - { - handler.Proxy = WebSession.Proxy; - } - - if (WebSession.Certificates is not null) - { - handler.ClientCertificates.AddRange(WebSession.Certificates); - } - - if (SkipCertificateCheck) - { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - handler.ClientCertificateOptions = ClientCertificateOption.Manual; - } - - // This indicates GetResponse will handle redirects. - if (handleRedirect || WebSession.MaximumRedirection == 0) - { - handler.AllowAutoRedirect = false; - } - else if (WebSession.MaximumRedirection > 0) - { - handler.MaxAutomaticRedirections = WebSession.MaximumRedirection; - } - - handler.SslProtocols = (SslProtocols)SslProtocol; - - HttpClient httpClient = new(handler); - - // Check timeout setting (in seconds instead of milliseconds as in HttpWebRequest) - httpClient.Timeout = TimeoutSec is 0 ? TimeSpan.FromMilliseconds(Timeout.Infinite) : new TimeSpan(0, 0, TimeoutSec); - - return httpClient; + return client; } internal virtual HttpRequestMessage GetRequest(Uri uri) @@ -1459,6 +1471,8 @@ namespace Microsoft.PowerShell.Commands } } + private bool IsPersistentSession() => MyInvocation.BoundParameters.ContainsKey(nameof(WebSession)) || MyInvocation.BoundParameters.ContainsKey(nameof(SessionVariable)); + /// /// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream. /// @@ -1686,7 +1700,7 @@ namespace Microsoft.PowerShell.Commands private static StringContent GetMultipartStringContent(object fieldName, object fieldValue) { ContentDispositionHeaderValue contentDisposition = new("form-data"); - + // .NET does not enclose field names in quotes, however, modern browsers and curl do. contentDisposition.Name = "\"" + LanguagePrimitives.ConvertTo(fieldName) + "\""; @@ -1773,7 +1787,7 @@ namespace Microsoft.PowerShell.Commands { // Ignore errors } - + if (string.IsNullOrEmpty(formattedError)) { // Remove HTML tags making it easier to read diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebProxy.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebProxy.cs index 0f46827bd6..6890599b51 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebProxy.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/CoreCLR/WebProxy.cs @@ -6,7 +6,7 @@ using System.Net; namespace Microsoft.PowerShell.Commands { - internal class WebProxy : IWebProxy + internal class WebProxy : IWebProxy, IEquatable { private ICredentials _credentials; private readonly Uri _proxyAddress; @@ -18,6 +18,23 @@ namespace Microsoft.PowerShell.Commands _proxyAddress = address; } + public override bool Equals(object obj) => Equals(obj as WebProxy); + + public override int GetHashCode() => HashCode.Combine(_proxyAddress, _credentials, BypassProxyOnLocal); + + public bool Equals(WebProxy other) + { + if (other is null) + { + return false; + } + + // _proxyAddress cannot be null as it is set in the constructor + return other._credentials == _credentials + && _proxyAddress.Equals(other._proxyAddress) + && BypassProxyOnLocal == other.BypassProxyOnLocal; + } + public ICredentials Credentials { get => _credentials; diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs index c100f345a7..4e58503659 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs @@ -4,15 +4,37 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; +using System.Threading; namespace Microsoft.PowerShell.Commands { /// /// WebRequestSession for holding session infos. /// - public class WebRequestSession + public class WebRequestSession : IDisposable { + private HttpClient _client; + private CookieContainer _cookies; + private bool _useDefaultCredentials; + private ICredentials _credentials; + private X509CertificateCollection _certificates; + private IWebProxy _proxy; + private int _maximumRedirection; + private WebSslProtocol _sslProtocol; + private bool _allowAutoRedirect; + private bool _skipCertificateCheck; + private bool _noProxy; + private bool _disposed; + private int _timeoutSec; + + /// + /// Contains true if an existing HttpClient had to be disposed and recreated since the WebSession was last used. + /// + private bool _disposedClient; + /// /// Gets or sets the Header property. /// @@ -27,25 +49,25 @@ namespace Microsoft.PowerShell.Commands /// /// Gets or sets the Cookies property. /// - public CookieContainer Cookies { get; set; } + public CookieContainer Cookies { get => _cookies; set => SetClassVar(ref _cookies, value); } #region Credentials /// /// Gets or sets the UseDefaultCredentials property. /// - public bool UseDefaultCredentials { get; set; } + public bool UseDefaultCredentials { get => _useDefaultCredentials; set => SetStructVar(ref _useDefaultCredentials, value); } /// /// Gets or sets the Credentials property. /// - public ICredentials Credentials { get; set; } + public ICredentials Credentials { get => _credentials; set => SetClassVar(ref _credentials, value); } /// /// Gets or sets the Certificates property. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] - public X509CertificateCollection Certificates { get; set; } + public X509CertificateCollection Certificates { get => _certificates; set => SetClassVar(ref _certificates, value); } #endregion @@ -57,12 +79,23 @@ namespace Microsoft.PowerShell.Commands /// /// Gets or sets the Proxy property. /// - public IWebProxy Proxy { get; set; } + public IWebProxy Proxy + { + get => _proxy; + set + { + SetClassVar(ref _proxy, value); + if (_proxy is not null) + { + NoProxy = false; + } + } + } /// - /// Gets or sets the RedirectMax property. + /// Gets or sets the MaximumRedirection property. /// - public int MaximumRedirection { get; set; } + public int MaximumRedirection { get => _maximumRedirection; set => SetStructVar(ref _maximumRedirection, value); } /// /// Gets or sets the count of retries for request failures. @@ -79,23 +112,42 @@ namespace Microsoft.PowerShell.Commands /// public WebRequestSession() { - // build the headers collection + // Build the headers collection Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); ContentHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - // build the cookie jar - Cookies = new CookieContainer(); + // Build the cookie jar + _cookies = new CookieContainer(); - // initialize the credential and certificate caches - UseDefaultCredentials = false; - Credentials = null; - Certificates = null; + // Initialize the credential and certificate caches + _useDefaultCredentials = false; + _credentials = null; + _certificates = null; - // setup the default UserAgent + // Setup the default UserAgent UserAgent = PSUserAgent.UserAgent; - Proxy = null; - MaximumRedirection = -1; + _proxy = null; + _maximumRedirection = -1; + _allowAutoRedirect = true; + } + + internal WebSslProtocol SslProtocol { set => SetStructVar(ref _sslProtocol, value); } + + internal bool SkipCertificateCheck { set => SetStructVar(ref _skipCertificateCheck, value); } + + internal int TimeoutSec { set => SetStructVar(ref _timeoutSec, value); } + + internal bool NoProxy + { + set + { + SetStructVar(ref _noProxy, value); + if (_noProxy) + { + Proxy = null; + } + } } /// @@ -105,8 +157,139 @@ namespace Microsoft.PowerShell.Commands internal void AddCertificate(X509Certificate certificate) { Certificates ??= new X509CertificateCollection(); + if (!Certificates.Contains(certificate)) + { + ResetClient(); + Certificates.Add(certificate); + } + } - Certificates.Add(certificate); + /// + /// Gets an existing or creates a new HttpClient for this WebRequest session if none currently exists (either because it was never + /// created, or because changes to the WebSession properties required the existing HttpClient to be disposed). + /// + /// True if the caller does not want the HttpClient to ever handle redirections automatically. + /// Contains true if an existing HttpClient had to be disposed and recreated since the WebSession was last used. + /// The HttpClient cached in the WebSession, based on all current settings. + internal HttpClient GetHttpClient(bool suppressHttpClientRedirects, out bool clientWasReset) + { + // Do not auto redirect if the the caller does not want it, or maximum redirections is 0 + SetStructVar(ref _allowAutoRedirect, !(suppressHttpClientRedirects || MaximumRedirection == 0)); + + clientWasReset = _disposedClient; + + if (_client is null) + { + _client = CreateHttpClient(); + _disposedClient = false; + } + + return _client; + } + + private HttpClient CreateHttpClient() + { + HttpClientHandler handler = new(); + + handler.CookieContainer = Cookies; + handler.AutomaticDecompression = DecompressionMethods.All; + + if (Credentials is not null) + { + handler.Credentials = Credentials; + } + else + { + handler.UseDefaultCredentials = UseDefaultCredentials; + } + + if (_noProxy) + { + handler.UseProxy = false; + } + else if (Proxy is not null) + { + handler.Proxy = Proxy; + } + + if (Certificates is not null) + { + handler.ClientCertificates.AddRange(Certificates); + } + + if (_skipCertificateCheck) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + handler.ClientCertificateOptions = ClientCertificateOption.Manual; + } + + handler.AllowAutoRedirect = _allowAutoRedirect; + if (_allowAutoRedirect && MaximumRedirection > 0) + { + handler.MaxAutomaticRedirections = MaximumRedirection; + } + + handler.SslProtocols = (SslProtocols)_sslProtocol; + + // Check timeout setting (in seconds instead of milliseconds as in HttpWebRequest) + return new HttpClient(handler) + { + Timeout = _timeoutSec is 0 ? TimeSpan.FromMilliseconds(Timeout.Infinite) : TimeSpan.FromSeconds(_timeoutSec) + }; + } + + private void SetClassVar(ref T oldValue, T newValue) where T : class + { + if (oldValue != newValue) + { + ResetClient(); + oldValue = newValue; + } + } + + private void SetStructVar(ref T oldValue, T newValue) where T : struct + { + if (!oldValue.Equals(newValue)) + { + ResetClient(); + oldValue = newValue; + } + } + + private void ResetClient() + { + if (_client is not null) + { + _disposedClient = true; + _client.Dispose(); + _client = null; + } + } + + /// + /// Dispose the WebRequestSession. + /// + /// True when called from Dispose() and false when called from finalizer. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _client?.Dispose(); + } + + _disposed = true; + } + } + + /// + /// Dispose the WebRequestSession. + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx index 99a1d0ef7c..5b1b33eae6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/WebCmdletStrings.resx @@ -249,4 +249,7 @@ Resulting JSON is truncated as serialization has exceeded the set depth of {0}. + + The WebSession properties were changed between requests forcing all HTTP connections in the session to be recreated. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 2109fc1fed..fe6db03a2a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -2250,6 +2250,148 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $pathologicalRatio | Should -BeGreaterThan 5 } } + + Context 'Invoke-WebSession: Connection persistence in a WebSession' { + # Match verbose message from resource name WebSessionConnectionRecreated with message: + # The WebSession properties were changed between requests forcing all HTTP connections in the session to be recreated. + $matchConnRecreatedMessage = [regex]::new('WebSession.+HTTP') + + function RunCheckingPersistence { + param( + [uri]$Uri, + [string]$Command, + [object]$Session, + [switch]$ExpectConnectionRecreated, + [switch]$CaptureSession + ) + + $pwsh = [PowerShell]::Create() + $pwsh.Runspace.SessionStateProxy.SetVariable('uri', $Uri) + if ($Session) { + $pwsh.Runspace.SessionStateProxy.SetVariable('Session', $Session) + $command = "$command -WebSession `$Session" + } + if ($CaptureSession) { + $command = "$command -SessionVariable Session" + } + $script = "`$null = $command -Verbose" + $pwsh.AddScript($script).Invoke() + $session = $pwsh.Runspace.SessionStateProxy.GetVariable('Session') + + $expectedConnRecreatedCount = if ($ExpectConnectionRecreated) { 1 } else { 0 } + ($pwsh.Streams.Verbose | Where-Object { $matchConnRecreatedMessage.Matches($_.Message) }).Count | Should -Be $expectedConnRecreatedCount + + $pwsh.Dispose() + + return $session + } + + It 'Connection persistence maintained' { + $uri = Get-WebListenerUrl + $Session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + 1 .. 3 | ForEach-Object { + RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $Session + } + } + + It 'Connection persistence impacted by changing SkipCertificateCheck' { + $uri = Get-WebListenerUrl -Https + # This first request will throw because the certificate is invalid + $session = $null + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + # No change in setting + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck:$false' -Session $session + # Skipping cert check changes persistence + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck' -Session $session -ExpectConnectionRecreated + # Same settings won't lose persistence + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck' -Session $session + # Lose persistence due to changing cert check - this will also throw + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SkipCertificateCheck:$false' -Session $session -ExpectConnectionRecreated + } + + It 'Connection persistence is not impacted by changing request headers' { + $uri = Get-WebListenerUrl + $session = $null + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{ A = "B" }' -Session $session + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{}' -Session $session + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -Headers @{ A = "C"; B = "D"}' -Session $session + } + + It 'Connection persistence is impacted by changing the session cookie jar' { + $uri = Get-WebListenerUrl + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + $session.Cookies = New-Object System.Net.CookieContainer + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session -ExpectConnectionRecreated + + # Adding a cookie to the container does not lose persistence + $Session.Cookies.Add('http://localhost', [system.net.cookie]::new('cookie', 'value')) + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + } + + It 'Connection persistence is not impacted by changing the user agent' { + $uri = Get-WebListenerUrl + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent Powershell' -CaptureSession + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent "PowerShell Core"' -Session $session + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -UserAgent "PowerShell Core with HttpClient"' -Session $session + # Ensure persistence is lost when we change a different setting + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy -UserAgent "PowerShell Core"' -Session $session -ExpectConnectionRecreated + } + + It 'Connection persistence is not impacted when NoProxy parameter is not supplied' { + $uri = Get-WebListenerUrl + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + # Explicitly prevent proxy - connection lost + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy' -Session $session -ExpectConnectionRecreated + # Provide explicit switch value - connection maintained + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -NoProxy:$true' -Session $session + # No spec for proxy - connection maintained + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + # Two follow up calls without altering anything do not lose connection + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + } + + It 'Connection persistence is not impacted when SslProtocol parameter is not supplied' { + $uri = Get-WebListenerUrl + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Tls12' -CaptureSession + # No SslProtocol provided - keeps last value - connection retained + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + # Explicit default - loses connection + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Default' -Session $session -ExpectConnectionRecreated + # No SslProtocol provided - keeps last value - connection retained + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -Session $session + # Explicitly set to same value as last time it was set - connection retained + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri -SslProtocol Default' -Session $session + } + + It 'Connection persistence is not impacted by Proxy and NoProxy unless changed parameters are used between invocations' { + $uri = Get-WebListenerUrl + $session = RunCheckingPersistence -Uri $uri -Command 'Invoke-WebRequest -Uri $uri' -CaptureSession + $proxy = 'http://127.0.0.1:8080' + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated + # same proxy - do not lose persistence + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session + # No proxy at all - use previous setting and don't lose connection + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session + + # NoProxy toggles proxy off - loses connection + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -NoProxy" -Session $session -ExpectConnectionRecreated + # No setting at all - retains NoProxy setting + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session + + # Use proxy again - lose connection + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated + # No proxy specified - connection retained + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session + + # Proxy changed - lose connection + $proxy = 'http://localhost:8080' + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri -proxy $proxy" -Session $session -ExpectConnectionRecreated + # No proxy specified - connection retained + $session = RunCheckingPersistence -Uri $uri -Command "Invoke-WebRequest -Uri $uri" -Session $session + } + } } Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" {