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" {