mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Merge partials in WebRequestPSCmdlet.Common.cs (#19126)
This commit is contained in:
+432
-443
@@ -88,8 +88,47 @@ namespace Microsoft.PowerShell.Commands
|
||||
/// <summary>
|
||||
/// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
|
||||
/// </summary>
|
||||
public abstract partial class WebRequestPSCmdlet : PSCmdlet
|
||||
public abstract class WebRequestPSCmdlet : PSCmdlet
|
||||
{
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token source.
|
||||
/// </summary>
|
||||
internal CancellationTokenSource _cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically follow Rel Links.
|
||||
/// </summary>
|
||||
internal bool _followRelLink = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of Rel Links to follow.
|
||||
/// </summary>
|
||||
internal int _maximumFollowRelLink = int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Parse Rel Links.
|
||||
/// </summary>
|
||||
internal bool _parseRelLink = false;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically follow Rel Links.
|
||||
/// </summary>
|
||||
internal Dictionary<string, string> _relationLink = null;
|
||||
|
||||
/// <summary>
|
||||
/// The current size of the local file being resumed.
|
||||
/// </summary>
|
||||
private long _resumeFileSize = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The remote endpoint returned a 206 status code indicating successful resume.
|
||||
/// </summary>
|
||||
private bool _resumeSuccess = false;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Virtual Properties
|
||||
|
||||
#region URI
|
||||
@@ -425,6 +464,213 @@ namespace Microsoft.PowerShell.Commands
|
||||
|
||||
#endregion Virtual Properties
|
||||
|
||||
#region Helper Properties
|
||||
|
||||
internal string QualifiedOutFile => QualifyFilePath(OutFile);
|
||||
|
||||
internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether writing to a file should Resume and append rather than overwrite.
|
||||
/// </summary>
|
||||
internal bool ShouldResume => Resume.IsPresent && _resumeSuccess;
|
||||
|
||||
internal bool ShouldSaveToOutFile => !string.IsNullOrEmpty(OutFile);
|
||||
|
||||
internal bool ShouldWriteToPipeline => !ShouldSaveToOutFile || PassThru;
|
||||
|
||||
#endregion Helper Properties
|
||||
|
||||
#region Abstract Methods
|
||||
|
||||
/// <summary>
|
||||
/// Read the supplied WebResponse object and push the resulting output into the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="response">Instance of a WebResponse object to be processed.</param>
|
||||
internal abstract void ProcessResponse(HttpResponseMessage response);
|
||||
|
||||
#endregion Abstract Methods
|
||||
|
||||
#region Overrides
|
||||
|
||||
/// <summary>
|
||||
/// The main execution method for cmdlets derived from WebRequestPSCmdlet.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To implement ^C.
|
||||
/// </summary>
|
||||
protected override void StopProcessing() => _cancelToken?.Cancel();
|
||||
|
||||
#endregion Overrides
|
||||
|
||||
#region Virtual Methods
|
||||
|
||||
internal virtual void ValidateParameters()
|
||||
@@ -684,236 +930,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether writing to a file should Resume and append rather than overwrite.
|
||||
/// </summary>
|
||||
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<IDictionary>(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
|
||||
|
||||
/// <summary>
|
||||
/// Exception class for webcmdlets to enable returning HTTP error response.
|
||||
/// </summary>
|
||||
public sealed class HttpResponseException : HttpRequestException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpResponseException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <param name="response">Response from the HTTP server.</param>
|
||||
public HttpResponseException(string message, HttpResponseMessage response) : base(message, inner: null, response.StatusCode)
|
||||
{
|
||||
Response = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP error response.
|
||||
/// </summary>
|
||||
public HttpResponseMessage Response { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
|
||||
/// </summary>
|
||||
public abstract partial class WebRequestPSCmdlet : PSCmdlet
|
||||
{
|
||||
#region Abstract Methods
|
||||
|
||||
/// <summary>
|
||||
/// Read the supplied WebResponse object and push the resulting output into the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="response">Instance of a WebResponse object to be processed.</param>
|
||||
internal abstract void ProcessResponse(HttpResponseMessage response);
|
||||
|
||||
#endregion Abstract Methods
|
||||
|
||||
/// <summary>
|
||||
/// Cancellation token source.
|
||||
/// </summary>
|
||||
internal CancellationTokenSource _cancelToken = null;
|
||||
|
||||
/// <summary>
|
||||
/// Parse Rel Links.
|
||||
/// </summary>
|
||||
internal bool _parseRelLink = false;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically follow Rel Links.
|
||||
/// </summary>
|
||||
internal bool _followRelLink = false;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically follow Rel Links.
|
||||
/// </summary>
|
||||
internal Dictionary<string, string> _relationLink = null;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of Rel Links to follow.
|
||||
/// </summary>
|
||||
internal int _maximumFollowRelLink = int.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The remote endpoint returned a 206 status code indicating successful resume.
|
||||
/// </summary>
|
||||
private bool _resumeSuccess = false;
|
||||
|
||||
/// <summary>
|
||||
/// The current size of the local file being resumed.
|
||||
/// </summary>
|
||||
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();
|
||||
@@ -1187,45 +1204,6 @@ namespace Microsoft.PowerShell.Commands
|
||||
}
|
||||
}
|
||||
|
||||
// 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 @@ namespace Microsoft.PowerShell.Commands
|
||||
|
||||
#endregion Virtual Methods
|
||||
|
||||
#region Overrides
|
||||
|
||||
/// <summary>
|
||||
/// The main execution method for cmdlets derived from WebRequestPSCmdlet.
|
||||
/// </summary>
|
||||
protected override void ProcessRecord()
|
||||
#region Helper Methods
|
||||
private Uri PrepareUri(Uri uri)
|
||||
{
|
||||
try
|
||||
uri = CheckProtocol(uri);
|
||||
|
||||
// Before creating the web request,
|
||||
// preprocess Body if content is a dictionary and method is GET (set as query)
|
||||
LanguagePrimitives.TryConvertTo<IDictionary>(Body, out IDictionary bodyAsDictionary);
|
||||
if (bodyAsDictionary is not null && (Method == WebRequestMethod.Default || Method == WebRequestMethod.Get || CustomMethod == "GET"))
|
||||
{
|
||||
// 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))
|
||||
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);
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To implement ^C.
|
||||
/// </summary>
|
||||
protected override void StopProcessing() => _cancelToken?.Cancel();
|
||||
private static Uri CheckProtocol(Uri uri)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(uri);
|
||||
|
||||
#endregion Overrides
|
||||
if (!uri.IsAbsoluteUri)
|
||||
{
|
||||
uri = new Uri("http://" + uri.OriginalString);
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
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}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
||||
/// </summary>
|
||||
@@ -1724,7 +1639,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
/// </summary>
|
||||
/// <param name="fieldName">The Field Name to use.</param>
|
||||
/// <param name="fieldValue">The Field Value to use.</param>
|
||||
/// <param name="formData">The <see cref="MultipartFormDataContent"/>> to update.</param>
|
||||
/// <param name="formData">The <see cref="MultipartFormDataContent"/> to update.</param>
|
||||
/// <param name="enumerate">If true, collection types in <paramref name="fieldValue"/> will be enumerated. If false, collections will be treated as single value.</param>
|
||||
private void AddMultipartContent(object fieldName, object fieldValue, MultipartFormDataContent formData, bool enumerate)
|
||||
{
|
||||
@@ -1815,6 +1730,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
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 @@ namespace Microsoft.PowerShell.Commands
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception class for webcmdlets to enable returning HTTP error response.
|
||||
/// </summary>
|
||||
public sealed class HttpResponseException : HttpRequestException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpResponseException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <param name="response">Response from the HTTP server.</param>
|
||||
public HttpResponseException(string message, HttpResponseMessage response) : base(message, inner: null, response.StatusCode)
|
||||
{
|
||||
Response = response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP error response.
|
||||
/// </summary>
|
||||
public HttpResponseMessage Response { get; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user