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 7779295c84d..a9c5934ecd3 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,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
@@ -425,6 +464,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;
+
+ 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()
@@ -684,241 +930,12 @@ internal virtual void PrepareSession()
WebSession.RetryIntervalInSeconds = RetryIntervalSec;
}
}
-
- #endregion Virtual Methods
-
- #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
-
- internal virtual HttpClient GetHttpClient(bool handleRedirect)
- {
- HttpClientHandler handler = new();
- handler.CookieContainer = WebSession.Cookies;
- handler.AutomaticDecompression = DecompressionMethods.All;
+
+ 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)
@@ -1187,45 +1204,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 code)
- {
- int intCode = (int)code;
- return
- (
- (intCode >= 300 && intCode < 304) ||
- intCode == 307 ||
- intCode == 308
- );
- }
-
- // 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)
- {
- return
- (
- code == HttpStatusCode.Found ||
- code == HttpStatusCode.Moved ||
- code == HttpStatusCode.Redirect ||
- code == HttpStatusCode.RedirectMethod ||
- code == HttpStatusCode.SeeOther ||
- code == HttpStatusCode.Ambiguous ||
- code == HttpStatusCode.MultipleChoices
- );
- }
-
- // Returns true if the status code shows a server or client error and MaximumRetryCount > 0
- private bool ShouldRetry(HttpStatusCode code)
- {
- int intCode = (int)code;
-
- return
- (
- (intCode == 304 || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 0
- );
- }
-
internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool handleRedirect)
{
ArgumentNullException.ThrowIfNull(client);
@@ -1368,188 +1346,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;
+ 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;
+ uriBuilder.Query = string.Concat(uriBuilder.Query.AsSpan(1), "&", FormatDictionary(bodyAsDictionary));
+ }
+ else
+ {
+ uriBuilder.Query = FormatDictionary(bodyAsDictionary);
+ }
- string reqVerboseMsg = string.Format(
- CultureInfo.CurrentCulture,
- WebCmdletStrings.WebMethodInvocationVerboseMsg,
- request.Version,
- request.Method,
- requestContentLength);
+ uri = uriBuilder.Uri;
- WriteVerbose(reqVerboseMsg);
+ // Set body to null to prevent later FillRequestStream
+ Body = null;
+ }
- using HttpResponseMessage response = GetResponse(client, request, handleRedirect);
+ return uri;
+ }
- string contentType = ContentHelper.GetContentType(response);
- string respVerboseMsg = string.Format(
- CultureInfo.CurrentCulture,
- WebCmdletStrings.WebResponseVerboseMsg,
- response.Content.Headers.ContentLength,
- contentType);
+ private static Uri CheckProtocol(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
- WriteVerbose(respVerboseMsg);
+ if (!uri.IsAbsoluteUri)
+ {
+ uri = new Uri("http://" + uri.OriginalString);
+ }
- bool _isSuccess = response.IsSuccessStatusCode;
+ return uri;
+ }
- // 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));
+ private string QualifyFilePath(string path)
+ {
+ string resolvedFilePath = PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true);
+ return resolvedFilePath;
+ }
- // Disable writing to the OutFile.
- OutFile = null;
- }
+ private static string FormatDictionary(IDictionary content)
+ {
+ ArgumentNullException.ThrowIfNull(content);
- if (ShouldCheckHttpStatus && !_isSuccess)
- {
- string message = string.Format(
- CultureInfo.CurrentCulture,
- WebCmdletStrings.ResponseStatusCodeFailure,
- (int)response.StatusCode,
- response.ReasonPhrase);
+ StringBuilder bodyBuilder = new();
+ foreach (string key in content.Keys)
+ {
+ if (bodyBuilder.Length > 0)
+ {
+ bodyBuilder.Append('&');
+ }
- 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();
- }
+ 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))
- {
- 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.
///
@@ -1724,7 +1639,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)
{
@@ -1815,6 +1730,7 @@ 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 + "\"";
@@ -1874,6 +1790,79 @@ private static string FormatErrorMessage(string error, string contentType)
return formattedError;
}
+ // 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
+ );
+ }
+
+ // 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)
+ {
+ return
+ (
+ code == HttpStatusCode.Found ||
+ code == HttpStatusCode.Moved ||
+ code == HttpStatusCode.Redirect ||
+ code == HttpStatusCode.RedirectMethod ||
+ code == HttpStatusCode.SeeOther ||
+ code == HttpStatusCode.Ambiguous ||
+ code == HttpStatusCode.MultipleChoices
+ );
+ }
+
+ // Returns true if the status code shows a server or client error and MaximumRetryCount > 0
+ private bool ShouldRetry(HttpStatusCode code)
+ {
+ int intCode = (int)code;
+
+ return
+ (
+ (intCode == 304 || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 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())
+ };
+
#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; }
+ }
}