1964 lines
78 KiB
C#
1964 lines
78 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Management.Automation;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Security;
|
|
using System.Security.Authentication;
|
|
using System.Security.Cryptography;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
|
|
namespace Microsoft.PowerShell.Commands
|
|
{
|
|
/// <summary>
|
|
/// The valid values for the -Authentication parameter for Invoke-RestMethod and Invoke-WebRequest.
|
|
/// </summary>
|
|
public enum WebAuthenticationType
|
|
{
|
|
/// <summary>
|
|
/// No authentication. Default.
|
|
/// </summary>
|
|
None,
|
|
|
|
/// <summary>
|
|
/// RFC-7617 Basic Authentication. Requires -Credential.
|
|
/// </summary>
|
|
Basic,
|
|
|
|
/// <summary>
|
|
/// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token.
|
|
/// </summary>
|
|
Bearer,
|
|
|
|
/// <summary>
|
|
/// RFC-6750 OAuth 2.0 Bearer Authentication. Requires -Token.
|
|
/// </summary>
|
|
OAuth,
|
|
}
|
|
|
|
// WebSslProtocol is used because not all SslProtocols are supported by HttpClientHandler.
|
|
// Also SslProtocols.Default is not the "default" for HttpClientHandler as SslProtocols.Ssl3 is not supported.
|
|
/// <summary>
|
|
/// The valid values for the -SslProtocol parameter for Invoke-RestMethod and Invoke-WebRequest.
|
|
/// </summary>
|
|
[Flags]
|
|
public enum WebSslProtocol
|
|
{
|
|
/// <summary>
|
|
/// No SSL protocol will be set and the system defaults will be used.
|
|
/// </summary>
|
|
Default = 0,
|
|
|
|
/// <summary>
|
|
/// Specifies the TLS 1.0 security protocol. The TLS protocol is defined in IETF RFC 2246.
|
|
/// </summary>
|
|
Tls = SslProtocols.Tls,
|
|
|
|
/// <summary>
|
|
/// Specifies the TLS 1.1 security protocol. The TLS protocol is defined in IETF RFC 4346.
|
|
/// </summary>
|
|
Tls11 = SslProtocols.Tls11,
|
|
|
|
/// <summary>
|
|
/// Specifies the TLS 1.2 security protocol. The TLS protocol is defined in IETF RFC 5246.
|
|
/// </summary>
|
|
Tls12 = SslProtocols.Tls12,
|
|
|
|
/// <summary>
|
|
/// Specifies the TLS 1.3 security protocol. The TLS protocol is defined in IETF RFC 8446.
|
|
/// </summary>
|
|
Tls13 = SslProtocols.Tls13
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base class for Invoke-RestMethod and Invoke-WebRequest commands.
|
|
/// </summary>
|
|
public abstract partial class WebRequestPSCmdlet : PSCmdlet
|
|
{
|
|
#region Virtual Properties
|
|
|
|
#region URI
|
|
|
|
/// <summary>
|
|
/// Deprecated. Gets or sets UseBasicParsing. This has no affect on the operation of the Cmdlet.
|
|
/// </summary>
|
|
[Parameter(DontShow = true)]
|
|
public virtual SwitchParameter UseBasicParsing { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Uri property.
|
|
/// </summary>
|
|
[Parameter(Position = 0, Mandatory = true)]
|
|
[ValidateNotNullOrEmpty]
|
|
public virtual Uri Uri { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Session
|
|
/// <summary>
|
|
/// Gets or sets the Session property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual WebRequestSession WebSession { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the SessionVariable property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Alias("SV")]
|
|
public virtual string SessionVariable { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Authorization and Credentials
|
|
|
|
/// <summary>
|
|
/// Gets or sets the AllowUnencryptedAuthentication property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter AllowUnencryptedAuthentication { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Authentication property used to determine the Authentication method for the web session.
|
|
/// Authentication does not work with UseDefaultCredentials.
|
|
/// Authentication over unencrypted sessions requires AllowUnencryptedAuthentication.
|
|
/// Basic: Requires Credential.
|
|
/// OAuth/Bearer: Requires Token.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual WebAuthenticationType Authentication { get; set; } = WebAuthenticationType.None;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Credential property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[Credential]
|
|
public virtual PSCredential Credential { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the UseDefaultCredentials property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter UseDefaultCredentials { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the CertificateThumbprint property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateNotNullOrEmpty]
|
|
public virtual string CertificateThumbprint { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Certificate property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateNotNull]
|
|
public virtual X509Certificate Certificate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the SkipCertificateCheck property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter SkipCertificateCheck { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TLS/SSL protocol used by the Web Cmdlet.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual WebSslProtocol SslProtocol { get; set; } = WebSslProtocol.Default;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Token property. Token is required by Authentication OAuth and Bearer.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SecureString Token { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Headers
|
|
|
|
/// <summary>
|
|
/// Gets or sets the UserAgent property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual string UserAgent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the DisableKeepAlive property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter DisableKeepAlive { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TimeOut property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(0, int.MaxValue)]
|
|
public virtual int TimeoutSec { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Headers property.
|
|
/// </summary>
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
|
|
[Parameter]
|
|
public virtual IDictionary Headers { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Redirect
|
|
|
|
/// <summary>
|
|
/// Gets or sets the RedirectMax property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(0, int.MaxValue)]
|
|
public virtual int MaximumRedirection
|
|
{
|
|
get { return _maximumRedirection; }
|
|
|
|
set { _maximumRedirection = value; }
|
|
}
|
|
|
|
private int _maximumRedirection = -1;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the MaximumRetryCount property, which determines the number of retries of a failed web request.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(0, int.MaxValue)]
|
|
public virtual int MaximumRetryCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the RetryIntervalSec property, which determines the number seconds between retries.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(1, int.MaxValue)]
|
|
public virtual int RetryIntervalSec { get; set; } = 5;
|
|
|
|
#endregion
|
|
|
|
#region Method
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Method property.
|
|
/// </summary>
|
|
[Parameter(ParameterSetName = "StandardMethod")]
|
|
[Parameter(ParameterSetName = "StandardMethodNoProxy")]
|
|
public virtual WebRequestMethod Method
|
|
{
|
|
get { return _method; }
|
|
|
|
set { _method = value; }
|
|
}
|
|
|
|
private WebRequestMethod _method = WebRequestMethod.Default;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the CustomMethod property.
|
|
/// </summary>
|
|
[Parameter(Mandatory = true, ParameterSetName = "CustomMethod")]
|
|
[Parameter(Mandatory = true, ParameterSetName = "CustomMethodNoProxy")]
|
|
[Alias("CM")]
|
|
[ValidateNotNullOrEmpty]
|
|
public virtual string CustomMethod
|
|
{
|
|
get { return _customMethod; }
|
|
|
|
set { _customMethod = value; }
|
|
}
|
|
|
|
private string _customMethod;
|
|
|
|
#endregion
|
|
|
|
#region NoProxy
|
|
|
|
/// <summary>
|
|
/// Gets or sets the NoProxy property.
|
|
/// </summary>
|
|
[Parameter(Mandatory = true, ParameterSetName = "CustomMethodNoProxy")]
|
|
[Parameter(Mandatory = true, ParameterSetName = "StandardMethodNoProxy")]
|
|
public virtual SwitchParameter NoProxy { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Proxy
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Proxy property.
|
|
/// </summary>
|
|
[Parameter(ParameterSetName = "StandardMethod")]
|
|
[Parameter(ParameterSetName = "CustomMethod")]
|
|
public virtual Uri Proxy { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the ProxyCredential property.
|
|
/// </summary>
|
|
[Parameter(ParameterSetName = "StandardMethod")]
|
|
[Parameter(ParameterSetName = "CustomMethod")]
|
|
[Credential]
|
|
public virtual PSCredential ProxyCredential { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the ProxyUseDefaultCredentials property.
|
|
/// </summary>
|
|
[Parameter(ParameterSetName = "StandardMethod")]
|
|
[Parameter(ParameterSetName = "CustomMethod")]
|
|
public virtual SwitchParameter ProxyUseDefaultCredentials { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region Input
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Body property.
|
|
/// </summary>
|
|
[Parameter(ValueFromPipeline = true)]
|
|
public virtual object Body { get; set; }
|
|
|
|
/// <summary>
|
|
/// Dictionary for use with RFC-7578 multipart/form-data submissions.
|
|
/// Keys are form fields and their respective values are form values.
|
|
/// A value may be a collection of form values or single form value.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual IDictionary Form { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the ContentType property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual string ContentType { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TransferEncoding property.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateSet("chunked", "compress", "deflate", "gzip", "identity", IgnoreCase = true)]
|
|
public virtual string TransferEncoding { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the InFile property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual string InFile { get; set; }
|
|
|
|
/// <summary>
|
|
/// Keep the original file path after the resolved provider path is assigned to InFile.
|
|
/// </summary>
|
|
private string _originalFilePath;
|
|
|
|
#endregion
|
|
|
|
#region Output
|
|
|
|
/// <summary>
|
|
/// Gets or sets the OutFile property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual string OutFile { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the PassThrough property.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter PassThru { get; set; }
|
|
|
|
/// <summary>
|
|
/// Resumes downloading a partial or incomplete file. OutFile is required.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter Resume { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to skip checking HTTP status for error codes.
|
|
/// </summary>
|
|
[Parameter]
|
|
public virtual SwitchParameter SkipHttpErrorCheck { get; set; }
|
|
|
|
#endregion
|
|
|
|
#endregion Virtual Properties
|
|
|
|
#region Virtual Methods
|
|
|
|
internal virtual void ValidateParameters()
|
|
{
|
|
// sessions
|
|
if ((WebSession != null) && (SessionVariable != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.SessionConflict,
|
|
"WebCmdletSessionConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// Authentication
|
|
if (UseDefaultCredentials && (Authentication != WebAuthenticationType.None))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationConflict,
|
|
"WebCmdletAuthenticationConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if ((Authentication != WebAuthenticationType.None) && (Token != null) && (Credential != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationTokenConflict,
|
|
"WebCmdletAuthenticationTokenConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if ((Authentication == WebAuthenticationType.Basic) && (Credential == null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationCredentialNotSupplied,
|
|
"WebCmdletAuthenticationCredentialNotSuppliedException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if ((Authentication == WebAuthenticationType.OAuth || Authentication == WebAuthenticationType.Bearer) && (Token == null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AuthenticationTokenNotSupplied,
|
|
"WebCmdletAuthenticationTokenNotSuppliedException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if (!AllowUnencryptedAuthentication && (Authentication != WebAuthenticationType.None) && (Uri.Scheme != "https"))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AllowUnencryptedAuthenticationRequired,
|
|
"WebCmdletAllowUnencryptedAuthenticationRequiredException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if (!AllowUnencryptedAuthentication && (Credential != null || UseDefaultCredentials) && (Uri.Scheme != "https"))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.AllowUnencryptedAuthenticationRequired,
|
|
"WebCmdletAllowUnencryptedAuthenticationRequiredException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// credentials
|
|
if (UseDefaultCredentials && (Credential != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.CredentialConflict,
|
|
"WebCmdletCredentialConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// Proxy server
|
|
if (ProxyUseDefaultCredentials && (ProxyCredential != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.ProxyCredentialConflict,
|
|
"WebCmdletProxyCredentialConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
else if ((Proxy == null) && ((ProxyCredential != null) || ProxyUseDefaultCredentials))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.ProxyUriNotSupplied,
|
|
"WebCmdletProxyUriNotSuppliedException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// request body content
|
|
if ((Body != null) && (InFile != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.BodyConflict,
|
|
"WebCmdletBodyConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if ((Body != null) && (Form != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.BodyFormConflict,
|
|
"WebCmdletBodyFormConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
if ((InFile != null) && (Form != null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.FormInFileConflict,
|
|
"WebCmdletFormInFileConflictException");
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// validate InFile path
|
|
if (InFile != null)
|
|
{
|
|
ProviderInfo provider = null;
|
|
ErrorRecord errorRecord = null;
|
|
|
|
try
|
|
{
|
|
Collection<string> providerPaths = GetResolvedProviderPathFromPSPath(InFile, out provider);
|
|
|
|
if (!provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
errorRecord = GetValidationError(WebCmdletStrings.NotFilesystemPath,
|
|
"WebCmdletInFileNotFilesystemPathException", InFile);
|
|
}
|
|
else
|
|
{
|
|
if (providerPaths.Count > 1)
|
|
{
|
|
errorRecord = GetValidationError(WebCmdletStrings.MultiplePathsResolved,
|
|
"WebCmdletInFileMultiplePathsResolvedException", InFile);
|
|
}
|
|
else if (providerPaths.Count == 0)
|
|
{
|
|
errorRecord = GetValidationError(WebCmdletStrings.NoPathResolved,
|
|
"WebCmdletInFileNoPathResolvedException", InFile);
|
|
}
|
|
else
|
|
{
|
|
if (Directory.Exists(providerPaths[0]))
|
|
{
|
|
errorRecord = GetValidationError(WebCmdletStrings.DirectoryPathSpecified,
|
|
"WebCmdletInFileNotFilePathException", InFile);
|
|
}
|
|
|
|
_originalFilePath = InFile;
|
|
InFile = providerPaths[0];
|
|
}
|
|
}
|
|
}
|
|
catch (ItemNotFoundException pathNotFound)
|
|
{
|
|
errorRecord = new ErrorRecord(pathNotFound.ErrorRecord, pathNotFound);
|
|
}
|
|
catch (ProviderNotFoundException providerNotFound)
|
|
{
|
|
errorRecord = new ErrorRecord(providerNotFound.ErrorRecord, providerNotFound);
|
|
}
|
|
catch (System.Management.Automation.DriveNotFoundException driveNotFound)
|
|
{
|
|
errorRecord = new ErrorRecord(driveNotFound.ErrorRecord, driveNotFound);
|
|
}
|
|
|
|
if (errorRecord != null)
|
|
{
|
|
ThrowTerminatingError(errorRecord);
|
|
}
|
|
}
|
|
|
|
// output ??
|
|
if (PassThru && (OutFile == null))
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.OutFileMissing,
|
|
"WebCmdletOutFileMissingException", nameof(PassThru));
|
|
ThrowTerminatingError(error);
|
|
}
|
|
|
|
// Resume requires OutFile.
|
|
if (Resume.IsPresent && OutFile == null)
|
|
{
|
|
ErrorRecord error = GetValidationError(WebCmdletStrings.OutFileMissing,
|
|
"WebCmdletOutFileMissingException", nameof(Resume));
|
|
ThrowTerminatingError(error);
|
|
}
|
|
}
|
|
|
|
internal virtual void PrepareSession()
|
|
{
|
|
// make sure we have a valid WebRequestSession object to work with
|
|
if (WebSession == null)
|
|
{
|
|
WebSession = new WebRequestSession();
|
|
}
|
|
|
|
if (SessionVariable != null)
|
|
{
|
|
// save the session back to the PS environment if requested
|
|
PSVariableIntrinsics vi = SessionState.PSVariable;
|
|
vi.Set(SessionVariable, WebSession);
|
|
}
|
|
|
|
//
|
|
// handle credentials
|
|
//
|
|
if (Credential != null && Authentication == WebAuthenticationType.None)
|
|
{
|
|
// get the relevant NetworkCredential
|
|
NetworkCredential netCred = Credential.GetNetworkCredential();
|
|
WebSession.Credentials = netCred;
|
|
|
|
// supplying a credential overrides the UseDefaultCredentials setting
|
|
WebSession.UseDefaultCredentials = false;
|
|
}
|
|
else if ((Credential != null || Token != null) && Authentication != WebAuthenticationType.None)
|
|
{
|
|
ProcessAuthentication();
|
|
}
|
|
else if (UseDefaultCredentials)
|
|
{
|
|
WebSession.UseDefaultCredentials = true;
|
|
}
|
|
|
|
if (CertificateThumbprint != null)
|
|
{
|
|
X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
|
|
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
|
|
X509Certificate2Collection collection = (X509Certificate2Collection)store.Certificates;
|
|
X509Certificate2Collection tbCollection = (X509Certificate2Collection)collection.Find(X509FindType.FindByThumbprint, CertificateThumbprint, false);
|
|
if (tbCollection.Count == 0)
|
|
{
|
|
CryptographicException ex = new(WebCmdletStrings.ThumbprintNotFound);
|
|
throw ex;
|
|
}
|
|
|
|
foreach (X509Certificate2 tbCert in tbCollection)
|
|
{
|
|
X509Certificate certificate = (X509Certificate)tbCert;
|
|
WebSession.AddCertificate(certificate);
|
|
}
|
|
}
|
|
|
|
if (Certificate != null)
|
|
{
|
|
WebSession.AddCertificate(Certificate);
|
|
}
|
|
|
|
//
|
|
// handle the user agent
|
|
//
|
|
if (UserAgent != null)
|
|
{
|
|
// store the UserAgent string
|
|
WebSession.UserAgent = UserAgent;
|
|
}
|
|
|
|
if (Proxy != null)
|
|
{
|
|
WebProxy webProxy = new(Proxy);
|
|
webProxy.BypassProxyOnLocal = false;
|
|
if (ProxyCredential != null)
|
|
{
|
|
webProxy.Credentials = ProxyCredential.GetNetworkCredential();
|
|
}
|
|
else if (ProxyUseDefaultCredentials)
|
|
{
|
|
// If both ProxyCredential and ProxyUseDefaultCredentials are passed,
|
|
// UseDefaultCredentials will overwrite the supplied credentials.
|
|
webProxy.UseDefaultCredentials = true;
|
|
}
|
|
|
|
WebSession.Proxy = webProxy;
|
|
}
|
|
|
|
if (MaximumRedirection > -1)
|
|
{
|
|
WebSession.MaximumRedirection = MaximumRedirection;
|
|
}
|
|
|
|
// store the other supplied headers
|
|
if (Headers != null)
|
|
{
|
|
foreach (string key in Headers.Keys)
|
|
{
|
|
var value = Headers[key];
|
|
|
|
// null is not valid value for header.
|
|
// We silently ignore header if value is null.
|
|
if (value is not null)
|
|
{
|
|
// add the header value (or overwrite it if already present)
|
|
WebSession.Headers[key] = value.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MaximumRetryCount > 0)
|
|
{
|
|
WebSession.MaximumRetryCount = MaximumRetryCount;
|
|
|
|
// only set retry interval if retry count is set.
|
|
WebSession.RetryIntervalInSeconds = RetryIntervalSec;
|
|
}
|
|
}
|
|
|
|
#endregion Virtual Methods
|
|
|
|
#region Helper Properties
|
|
|
|
internal string QualifiedOutFile
|
|
{
|
|
get { return (QualifyFilePath(OutFile)); }
|
|
}
|
|
|
|
internal bool ShouldSaveToOutFile
|
|
{
|
|
get { return (!string.IsNullOrEmpty(OutFile)); }
|
|
}
|
|
|
|
internal bool ShouldWriteToPipeline
|
|
{
|
|
get { return (!ShouldSaveToOutFile || PassThru); }
|
|
}
|
|
|
|
internal bool ShouldCheckHttpStatus
|
|
{
|
|
get { return !SkipHttpErrorCheck; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether writing to a file should Resume and append rather than overwrite.
|
|
/// </summary>
|
|
internal bool ShouldResume
|
|
{
|
|
get { return (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)
|
|
IDictionary bodyAsDictionary;
|
|
LanguagePrimitives.TryConvertTo<IDictionary>(Body, out bodyAsDictionary);
|
|
if ((bodyAsDictionary != null)
|
|
&& ((IsStandardMethodSet() && (Method == WebRequestMethod.Default || Method == WebRequestMethod.Get))
|
|
|| (IsCustomMethodSet() && CustomMethod.ToUpperInvariant() == "GET")))
|
|
{
|
|
UriBuilder uriBuilder = new(uri);
|
|
if (uriBuilder.Query != 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)
|
|
{
|
|
if (uri == null) { throw new ArgumentNullException(nameof(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)
|
|
{
|
|
if (content == null)
|
|
throw new ArgumentNullException(nameof(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 != null)
|
|
{
|
|
encodedValue = WebUtility.UrlEncode(value.ToString());
|
|
}
|
|
|
|
bodyBuilder.AppendFormat("{0}={1}", 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 bool IsStandardMethodSet()
|
|
{
|
|
return (ParameterSetName == "StandardMethod" || ParameterSetName == "StandardMethodNoProxy");
|
|
}
|
|
|
|
private bool IsCustomMethodSet()
|
|
{
|
|
return (ParameterSetName == "CustomMethod" || ParameterSetName == "CustomMethodNoProxy");
|
|
}
|
|
|
|
private string GetBasicAuthorizationHeader()
|
|
{
|
|
var password = new NetworkCredential(null, Credential.Password).Password;
|
|
string unencoded = string.Format("{0}:{1}", Credential.UserName, password);
|
|
byte[] bytes = Encoding.UTF8.GetBytes(unencoded);
|
|
return string.Format("Basic {0}", Convert.ToBase64String(bytes));
|
|
}
|
|
|
|
private string GetBearerAuthorizationHeader()
|
|
{
|
|
return string.Format("Bearer {0}", 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.Format("Unrecognized Authentication value: {0}", 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)
|
|
{
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the PreserveAuthorizationOnRedirect property.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property overrides compatibility with web requests on Windows.
|
|
/// On FullCLR (WebRequest), authorization headers are stripped during redirect.
|
|
/// CoreCLR (HTTPClient) does not have this behavior so web requests that work on
|
|
/// PowerShell/FullCLR can fail with PowerShell/CoreCLR. To provide compatibility,
|
|
/// we'll detect requests with an Authorization header and automatically strip
|
|
/// the header when the first redirect occurs. This switch turns off this logic for
|
|
/// edge cases where the authorization header needs to be preserved across redirects.
|
|
/// </remarks>
|
|
[Parameter]
|
|
public virtual SwitchParameter PreserveAuthorizationOnRedirect { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the SkipHeaderValidation property.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This property adds headers to the request's header collection without validation.
|
|
/// </remarks>
|
|
[Parameter]
|
|
public virtual SwitchParameter SkipHeaderValidation { get; set; }
|
|
|
|
#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 HttpMethod GetHttpMethod(WebRequestMethod method)
|
|
{
|
|
switch (Method)
|
|
{
|
|
case WebRequestMethod.Default:
|
|
case WebRequestMethod.Get:
|
|
return HttpMethod.Get;
|
|
case WebRequestMethod.Head:
|
|
return HttpMethod.Head;
|
|
case WebRequestMethod.Post:
|
|
return HttpMethod.Post;
|
|
case WebRequestMethod.Put:
|
|
return HttpMethod.Put;
|
|
case WebRequestMethod.Delete:
|
|
return HttpMethod.Delete;
|
|
case WebRequestMethod.Trace:
|
|
return HttpMethod.Trace;
|
|
case WebRequestMethod.Options:
|
|
return HttpMethod.Options;
|
|
default:
|
|
// Merge and Patch
|
|
return new HttpMethod(Method.ToString().ToUpperInvariant());
|
|
}
|
|
}
|
|
|
|
#region Virtual Methods
|
|
|
|
// NOTE: Only pass true for handleRedirect if the original request has an authorization header
|
|
// and PreserveAuthorizationOnRedirect is NOT set.
|
|
internal virtual HttpClient GetHttpClient(bool handleRedirect)
|
|
{
|
|
// By default the HttpClientHandler will automatically decompress GZip and Deflate content
|
|
HttpClientHandler handler = new();
|
|
handler.CookieContainer = WebSession.Cookies;
|
|
|
|
// set the credentials used by this request
|
|
if (WebSession.UseDefaultCredentials)
|
|
{
|
|
// the UseDefaultCredentials flag overrides other supplied credentials
|
|
handler.UseDefaultCredentials = true;
|
|
}
|
|
else if (WebSession.Credentials != null)
|
|
{
|
|
handler.Credentials = WebSession.Credentials;
|
|
}
|
|
|
|
if (NoProxy)
|
|
{
|
|
handler.UseProxy = false;
|
|
}
|
|
else if (WebSession.Proxy != null)
|
|
{
|
|
handler.Proxy = WebSession.Proxy;
|
|
}
|
|
|
|
if (WebSession.Certificates != null)
|
|
{
|
|
handler.ClientCertificates.AddRange(WebSession.Certificates);
|
|
}
|
|
|
|
if (SkipCertificateCheck)
|
|
{
|
|
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
|
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
|
|
}
|
|
|
|
// This indicates GetResponse will handle redirects.
|
|
if (handleRedirect)
|
|
{
|
|
handler.AllowAutoRedirect = false;
|
|
}
|
|
else if (WebSession.MaximumRedirection > -1)
|
|
{
|
|
if (WebSession.MaximumRedirection == 0)
|
|
{
|
|
handler.AllowAutoRedirect = false;
|
|
}
|
|
else
|
|
{
|
|
handler.MaxAutomaticRedirections = WebSession.MaximumRedirection;
|
|
}
|
|
}
|
|
|
|
handler.SslProtocols = (SslProtocols)SslProtocol;
|
|
|
|
HttpClient httpClient = new(handler);
|
|
|
|
// check timeout setting (in seconds instead of milliseconds as in HttpWebRequest)
|
|
if (TimeoutSec == 0)
|
|
{
|
|
// A zero timeout means infinite
|
|
httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
|
}
|
|
else if (TimeoutSec > 0)
|
|
{
|
|
httpClient.Timeout = new TimeSpan(0, 0, TimeoutSec);
|
|
}
|
|
|
|
return httpClient;
|
|
}
|
|
|
|
internal virtual HttpRequestMessage GetRequest(Uri uri)
|
|
{
|
|
Uri requestUri = PrepareUri(uri);
|
|
HttpMethod httpMethod = null;
|
|
|
|
switch (ParameterSetName)
|
|
{
|
|
case "StandardMethodNoProxy":
|
|
goto case "StandardMethod";
|
|
case "StandardMethod":
|
|
// set the method if the parameter was provided
|
|
httpMethod = GetHttpMethod(Method);
|
|
break;
|
|
case "CustomMethodNoProxy":
|
|
goto case "CustomMethod";
|
|
case "CustomMethod":
|
|
if (!string.IsNullOrEmpty(CustomMethod))
|
|
{
|
|
// set the method if the parameter was provided
|
|
httpMethod = new HttpMethod(CustomMethod.ToUpperInvariant());
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// create the base WebRequest object
|
|
var request = new HttpRequestMessage(httpMethod, requestUri);
|
|
|
|
// pull in session data
|
|
if (WebSession.Headers.Count > 0)
|
|
{
|
|
WebSession.ContentHeaders.Clear();
|
|
foreach (var entry in WebSession.Headers)
|
|
{
|
|
if (HttpKnownHeaderNames.ContentHeaders.Contains(entry.Key))
|
|
{
|
|
WebSession.ContentHeaders.Add(entry.Key, entry.Value);
|
|
}
|
|
else
|
|
{
|
|
if (SkipHeaderValidation)
|
|
{
|
|
request.Headers.TryAddWithoutValidation(entry.Key, entry.Value);
|
|
}
|
|
else
|
|
{
|
|
request.Headers.Add(entry.Key, entry.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set 'Transfer-Encoding: chunked' if 'Transfer-Encoding' is specified
|
|
if (WebSession.Headers.ContainsKey(HttpKnownHeaderNames.TransferEncoding))
|
|
{
|
|
request.Headers.TransferEncodingChunked = true;
|
|
}
|
|
|
|
// Set 'User-Agent' if WebSession.Headers doesn't already contain it
|
|
string userAgent = null;
|
|
if (WebSession.Headers.TryGetValue(HttpKnownHeaderNames.UserAgent, out userAgent))
|
|
{
|
|
WebSession.UserAgent = userAgent;
|
|
}
|
|
else
|
|
{
|
|
if (SkipHeaderValidation)
|
|
{
|
|
request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.UserAgent, WebSession.UserAgent);
|
|
}
|
|
else
|
|
{
|
|
request.Headers.Add(HttpKnownHeaderNames.UserAgent, WebSession.UserAgent);
|
|
}
|
|
}
|
|
|
|
// Set 'Keep-Alive' to false. This means set the Connection to 'Close'.
|
|
if (DisableKeepAlive)
|
|
{
|
|
request.Headers.Add(HttpKnownHeaderNames.Connection, "Close");
|
|
}
|
|
|
|
// Set 'Transfer-Encoding'
|
|
if (TransferEncoding != null)
|
|
{
|
|
request.Headers.TransferEncodingChunked = true;
|
|
var headerValue = new TransferCodingHeaderValue(TransferEncoding);
|
|
if (!request.Headers.TransferEncoding.Contains(headerValue))
|
|
{
|
|
request.Headers.TransferEncoding.Add(headerValue);
|
|
}
|
|
}
|
|
|
|
// If the file to resume downloading exists, create the Range request header using the file size.
|
|
// If not, create a Range to request the entire file.
|
|
if (Resume.IsPresent)
|
|
{
|
|
var fileInfo = new FileInfo(QualifiedOutFile);
|
|
if (fileInfo.Exists)
|
|
{
|
|
request.Headers.Range = new RangeHeaderValue(fileInfo.Length, null);
|
|
_resumeFileSize = fileInfo.Length;
|
|
}
|
|
else
|
|
{
|
|
request.Headers.Range = new RangeHeaderValue(0, null);
|
|
}
|
|
}
|
|
|
|
return (request);
|
|
}
|
|
|
|
internal virtual void FillRequestStream(HttpRequestMessage request)
|
|
{
|
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
|
|
|
// set the content type
|
|
if (ContentType != null)
|
|
{
|
|
WebSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = ContentType;
|
|
// request
|
|
}
|
|
// ContentType == null
|
|
else if (Method == WebRequestMethod.Post || (IsCustomMethodSet() && CustomMethod.ToUpperInvariant() == "POST"))
|
|
{
|
|
// Win8:545310 Invoke-WebRequest does not properly set MIME type for POST
|
|
string contentType = null;
|
|
WebSession.ContentHeaders.TryGetValue(HttpKnownHeaderNames.ContentType, out contentType);
|
|
if (string.IsNullOrEmpty(contentType))
|
|
{
|
|
WebSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = "application/x-www-form-urlencoded";
|
|
}
|
|
}
|
|
|
|
if (Form != null)
|
|
{
|
|
// Content headers will be set by MultipartFormDataContent which will throw unless we clear them first
|
|
WebSession.ContentHeaders.Clear();
|
|
|
|
var formData = new MultipartFormDataContent();
|
|
foreach (DictionaryEntry formEntry in Form)
|
|
{
|
|
// AddMultipartContent will handle PSObject unwrapping, Object type determination and enumerateing top level IEnumerables.
|
|
AddMultipartContent(fieldName: formEntry.Key, fieldValue: formEntry.Value, formData: formData, enumerate: true);
|
|
}
|
|
|
|
SetRequestContent(request, formData);
|
|
}
|
|
// coerce body into a usable form
|
|
else if (Body != null)
|
|
{
|
|
object content = Body;
|
|
|
|
// make sure we're using the base object of the body, not the PSObject wrapper
|
|
PSObject psBody = Body as PSObject;
|
|
if (psBody != null)
|
|
{
|
|
content = psBody.BaseObject;
|
|
}
|
|
|
|
if (content is FormObject form)
|
|
{
|
|
SetRequestContent(request, form.Fields);
|
|
}
|
|
else if (content is IDictionary dictionary && request.Method != HttpMethod.Get)
|
|
{
|
|
SetRequestContent(request, dictionary);
|
|
}
|
|
else if (content is XmlNode xmlNode)
|
|
{
|
|
SetRequestContent(request, xmlNode);
|
|
}
|
|
else if (content is Stream stream)
|
|
{
|
|
SetRequestContent(request, stream);
|
|
}
|
|
else if (content is byte[] bytes)
|
|
{
|
|
SetRequestContent(request, bytes);
|
|
}
|
|
else if (content is MultipartFormDataContent multipartFormDataContent)
|
|
{
|
|
WebSession.ContentHeaders.Clear();
|
|
SetRequestContent(request, multipartFormDataContent);
|
|
}
|
|
else
|
|
{
|
|
SetRequestContent(
|
|
request,
|
|
(string)LanguagePrimitives.ConvertTo(content, typeof(string), CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
else if (InFile != null) // copy InFile data
|
|
{
|
|
try
|
|
{
|
|
// open the input file
|
|
SetRequestContent(request, new FileStream(InFile, FileMode.Open, FileAccess.Read, FileShare.Read));
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
string msg = string.Format(CultureInfo.InvariantCulture, WebCmdletStrings.AccessDenied,
|
|
_originalFilePath);
|
|
throw new UnauthorizedAccessException(msg);
|
|
}
|
|
}
|
|
|
|
// Add the content headers
|
|
if (request.Content == null)
|
|
{
|
|
request.Content = new StringContent(string.Empty);
|
|
request.Content.Headers.Clear();
|
|
}
|
|
|
|
foreach (var entry in WebSession.ContentHeaders)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(entry.Value))
|
|
{
|
|
if (SkipHeaderValidation)
|
|
{
|
|
request.Content.Headers.TryAddWithoutValidation(entry.Key, entry.Value);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
request.Content.Headers.Add(entry.Key, entry.Value);
|
|
}
|
|
catch (FormatException ex)
|
|
{
|
|
var outerEx = new ValidationMetadataException(WebCmdletStrings.ContentTypeException, ex);
|
|
ErrorRecord er = new(outerEx, "WebCmdletContentTypeException", ErrorCategory.InvalidArgument, ContentType);
|
|
ThrowTerminatingError(er);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
);
|
|
}
|
|
|
|
// 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
|
|
);
|
|
}
|
|
|
|
private bool ShouldRetry(HttpStatusCode code)
|
|
{
|
|
int intCode = (int)code;
|
|
|
|
if (((intCode == 304) || (intCode >= 400 && intCode <= 599)) && WebSession.MaximumRetryCount > 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool keepAuthorization)
|
|
{
|
|
if (client == null) { throw new ArgumentNullException(nameof(client)); }
|
|
|
|
if (request == null) { throw new ArgumentNullException(nameof(request)); }
|
|
|
|
// Add 1 to account for the first request.
|
|
int totalRequests = WebSession.MaximumRetryCount + 1;
|
|
HttpRequestMessage req = request;
|
|
HttpResponseMessage response = null;
|
|
|
|
do
|
|
{
|
|
// Track the current URI being used by various requests and re-requests.
|
|
var currentUri = req.RequestUri;
|
|
|
|
_cancelToken = new CancellationTokenSource();
|
|
response = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
|
|
|
|
if (keepAuthorization && IsRedirectCode(response.StatusCode) && response.Headers.Location != null)
|
|
{
|
|
_cancelToken.Cancel();
|
|
_cancelToken = null;
|
|
|
|
// if explicit count was provided, reduce it for this redirection.
|
|
if (WebSession.MaximumRedirection > 0)
|
|
{
|
|
WebSession.MaximumRedirection--;
|
|
}
|
|
// For selected redirects that used POST, GET must be used with the
|
|
// redirected Location.
|
|
// Since GET is the default; POST only occurs when -Method POST is used.
|
|
if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode))
|
|
{
|
|
// See https://msdn.microsoft.com/library/system.net.httpstatuscode(v=vs.110).aspx
|
|
Method = WebRequestMethod.Get;
|
|
}
|
|
|
|
currentUri = new Uri(request.RequestUri, response.Headers.Location);
|
|
// Continue to handle redirection
|
|
using (client = GetHttpClient(handleRedirect: true))
|
|
using (HttpRequestMessage redirectRequest = GetRequest(currentUri))
|
|
{
|
|
response = GetResponse(client, redirectRequest, keepAuthorization);
|
|
}
|
|
}
|
|
|
|
// Request again without the Range header because the server indicated the range was not satisfiable.
|
|
// This happens when the local file is larger than the remote file.
|
|
// If the size of the remote file is the same as the local file, there is nothing to resume.
|
|
if (Resume.IsPresent &&
|
|
response.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable &&
|
|
(response.Content.Headers.ContentRange.HasLength &&
|
|
response.Content.Headers.ContentRange.Length != _resumeFileSize))
|
|
{
|
|
_cancelToken.Cancel();
|
|
|
|
WriteVerbose(WebCmdletStrings.WebMethodResumeFailedVerboseMsg);
|
|
|
|
// Disable the Resume switch so the subsequent calls to GetResponse() and FillRequestStream()
|
|
// are treated as a standard -OutFile request. This also disables appending local file.
|
|
Resume = new SwitchParameter(false);
|
|
|
|
using (HttpRequestMessage requestWithoutRange = GetRequest(currentUri))
|
|
{
|
|
FillRequestStream(requestWithoutRange);
|
|
long requestContentLength = 0;
|
|
if (requestWithoutRange.Content != null)
|
|
{
|
|
requestContentLength = requestWithoutRange.Content.Headers.ContentLength.Value;
|
|
}
|
|
|
|
string reqVerboseMsg = string.Format(
|
|
CultureInfo.CurrentCulture,
|
|
WebCmdletStrings.WebMethodInvocationVerboseMsg,
|
|
requestWithoutRange.Method,
|
|
requestWithoutRange.RequestUri,
|
|
requestContentLength);
|
|
WriteVerbose(reqVerboseMsg);
|
|
|
|
return GetResponse(client, requestWithoutRange, keepAuthorization);
|
|
}
|
|
}
|
|
|
|
_resumeSuccess = response.StatusCode == HttpStatusCode.PartialContent;
|
|
|
|
// When MaximumRetryCount is not specified, the totalRequests == 1.
|
|
if (totalRequests > 1 && ShouldRetry(response.StatusCode))
|
|
{
|
|
string retryMessage = string.Format(
|
|
CultureInfo.CurrentCulture,
|
|
WebCmdletStrings.RetryVerboseMsg,
|
|
RetryIntervalSec,
|
|
response.StatusCode);
|
|
|
|
WriteVerbose(retryMessage);
|
|
|
|
_cancelToken = new CancellationTokenSource();
|
|
Task.Delay(WebSession.RetryIntervalInSeconds * 1000, _cancelToken.Token).GetAwaiter().GetResult();
|
|
_cancelToken.Cancel();
|
|
_cancelToken = null;
|
|
|
|
req.Dispose();
|
|
req = GetRequest(currentUri);
|
|
FillRequestStream(req);
|
|
}
|
|
|
|
totalRequests--;
|
|
}
|
|
while (totalRequests > 0 && !response.IsSuccessStatusCode);
|
|
|
|
return response;
|
|
}
|
|
|
|
internal virtual void UpdateSession(HttpResponseMessage response)
|
|
{
|
|
if (response == null) { throw new ArgumentNullException(nameof(response)); }
|
|
}
|
|
|
|
#endregion Virtual 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 keepAuthorization = WebSession != null
|
|
&&
|
|
WebSession.Headers != null
|
|
&&
|
|
PreserveAuthorizationOnRedirect.IsPresent
|
|
&&
|
|
WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization);
|
|
|
|
using (HttpClient client = GetHttpClient(keepAuthorization))
|
|
{
|
|
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 = 0;
|
|
if (request.Content != null)
|
|
requestContentLength = request.Content.Headers.ContentLength.Value;
|
|
|
|
string reqVerboseMsg = string.Format(CultureInfo.CurrentCulture,
|
|
WebCmdletStrings.WebMethodInvocationVerboseMsg,
|
|
request.Method,
|
|
requestContentLength);
|
|
WriteVerbose(reqVerboseMsg);
|
|
|
|
HttpResponseMessage response = GetResponse(client, request, keepAuthorization);
|
|
|
|
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));
|
|
// remove HTML tags making it easier to read
|
|
detailMsg = System.Text.RegularExpressions.Regex.Replace(reader.ReadToEnd(), "<[^>]*>", string.Empty);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// catch all
|
|
}
|
|
finally
|
|
{
|
|
if (reader != null)
|
|
{
|
|
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) // Indicate "HttpClientHandler.AllowAutoRedirect == false"
|
|
{
|
|
if (response.StatusCode == HttpStatusCode.Found ||
|
|
response.StatusCode == HttpStatusCode.Moved ||
|
|
response.StatusCode == HttpStatusCode.MovedPermanently)
|
|
{
|
|
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 != 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>
|
|
/// Implementing ^C, after start the BeginGetResponse.
|
|
/// </summary>
|
|
protected override void StopProcessing()
|
|
{
|
|
if (_cancelToken != null)
|
|
{
|
|
_cancelToken.Cancel();
|
|
}
|
|
}
|
|
|
|
#endregion Overrides
|
|
|
|
#region Helper Methods
|
|
|
|
/// <summary>
|
|
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
|
/// </summary>
|
|
/// <param name="request">The WebRequest who's content is to be set.</param>
|
|
/// <param name="content">A byte array containing the content data.</param>
|
|
/// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property.</returns>
|
|
/// <remarks>
|
|
/// Because this function sets the request's ContentLength property and writes content data into the requests's stream,
|
|
/// it should be called one time maximum on a given request.
|
|
/// </remarks>
|
|
internal long SetRequestContent(HttpRequestMessage request, byte[] content)
|
|
{
|
|
if (request == null)
|
|
throw new ArgumentNullException(nameof(request));
|
|
if (content == null)
|
|
return 0;
|
|
|
|
var byteArrayContent = new ByteArrayContent(content);
|
|
request.Content = byteArrayContent;
|
|
|
|
return byteArrayContent.Headers.ContentLength.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
|
/// </summary>
|
|
/// <param name="request">The WebRequest who's content is to be set.</param>
|
|
/// <param name="content">A String object containing the content data.</param>
|
|
/// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property.</returns>
|
|
/// <remarks>
|
|
/// Because this function sets the request's ContentLength property and writes content data into the requests's stream,
|
|
/// it should be called one time maximum on a given request.
|
|
/// </remarks>
|
|
internal long SetRequestContent(HttpRequestMessage request, string content)
|
|
{
|
|
if (request == null)
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
if (content == null)
|
|
return 0;
|
|
|
|
Encoding encoding = null;
|
|
if (ContentType != null)
|
|
{
|
|
// If Content-Type contains the encoding format (as CharSet), use this encoding format
|
|
// to encode the Body of the WebRequest sent to the server. Default Encoding format
|
|
// would be used if Charset is not supplied in the Content-Type property.
|
|
try
|
|
{
|
|
var mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(ContentType);
|
|
if (!string.IsNullOrEmpty(mediaTypeHeaderValue.CharSet))
|
|
{
|
|
encoding = Encoding.GetEncoding(mediaTypeHeaderValue.CharSet);
|
|
}
|
|
}
|
|
catch (FormatException ex)
|
|
{
|
|
if (!SkipHeaderValidation)
|
|
{
|
|
var outerEx = new ValidationMetadataException(WebCmdletStrings.ContentTypeException, ex);
|
|
ErrorRecord er = new(outerEx, "WebCmdletContentTypeException", ErrorCategory.InvalidArgument, ContentType);
|
|
ThrowTerminatingError(er);
|
|
}
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
if (!SkipHeaderValidation)
|
|
{
|
|
var outerEx = new ValidationMetadataException(WebCmdletStrings.ContentTypeException, ex);
|
|
ErrorRecord er = new(outerEx, "WebCmdletContentTypeException", ErrorCategory.InvalidArgument, ContentType);
|
|
ThrowTerminatingError(er);
|
|
}
|
|
}
|
|
}
|
|
|
|
byte[] bytes = StreamHelper.EncodeToBytes(content, encoding);
|
|
var byteArrayContent = new ByteArrayContent(bytes);
|
|
request.Content = byteArrayContent;
|
|
|
|
return byteArrayContent.Headers.ContentLength.Value;
|
|
}
|
|
|
|
internal long SetRequestContent(HttpRequestMessage request, XmlNode xmlNode)
|
|
{
|
|
if (request == null)
|
|
throw new ArgumentNullException(nameof(request));
|
|
|
|
if (xmlNode == null)
|
|
return 0;
|
|
|
|
byte[] bytes = null;
|
|
XmlDocument doc = xmlNode as XmlDocument;
|
|
if (doc?.FirstChild is XmlDeclaration)
|
|
{
|
|
XmlDeclaration decl = doc.FirstChild as XmlDeclaration;
|
|
Encoding encoding = Encoding.GetEncoding(decl.Encoding);
|
|
bytes = StreamHelper.EncodeToBytes(doc.OuterXml, encoding);
|
|
}
|
|
else
|
|
{
|
|
bytes = StreamHelper.EncodeToBytes(xmlNode.OuterXml);
|
|
}
|
|
|
|
var byteArrayContent = new ByteArrayContent(bytes);
|
|
request.Content = byteArrayContent;
|
|
|
|
return byteArrayContent.Headers.ContentLength.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
|
/// </summary>
|
|
/// <param name="request">The WebRequest who's content is to be set.</param>
|
|
/// <param name="contentStream">A Stream object containing the content data.</param>
|
|
/// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property.</returns>
|
|
/// <remarks>
|
|
/// Because this function sets the request's ContentLength property and writes content data into the requests's stream,
|
|
/// it should be called one time maximum on a given request.
|
|
/// </remarks>
|
|
internal long SetRequestContent(HttpRequestMessage request, Stream contentStream)
|
|
{
|
|
if (request == null)
|
|
throw new ArgumentNullException(nameof(request));
|
|
if (contentStream == null)
|
|
throw new ArgumentNullException(nameof(contentStream));
|
|
|
|
var streamContent = new StreamContent(contentStream);
|
|
request.Content = streamContent;
|
|
|
|
return streamContent.Headers.ContentLength.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the ContentLength property of the request and writes the specified content to the request's RequestStream.
|
|
/// </summary>
|
|
/// <param name="request">The WebRequest who's content is to be set.</param>
|
|
/// <param name="multipartContent">A MultipartFormDataContent object containing multipart/form-data content.</param>
|
|
/// <returns>The number of bytes written to the requests RequestStream (and the new value of the request's ContentLength property.</returns>
|
|
/// <remarks>
|
|
/// Because this function sets the request's ContentLength property and writes content data into the requests's stream,
|
|
/// it should be called one time maximum on a given request.
|
|
/// </remarks>
|
|
internal long SetRequestContent(HttpRequestMessage request, MultipartFormDataContent multipartContent)
|
|
{
|
|
if (request == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(request));
|
|
}
|
|
|
|
if (multipartContent == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(multipartContent));
|
|
}
|
|
|
|
request.Content = multipartContent;
|
|
|
|
return multipartContent.Headers.ContentLength.Value;
|
|
}
|
|
|
|
internal long SetRequestContent(HttpRequestMessage request, IDictionary content)
|
|
{
|
|
if (request == null)
|
|
throw new ArgumentNullException(nameof(request));
|
|
if (content == null)
|
|
throw new ArgumentNullException(nameof(content));
|
|
|
|
string body = FormatDictionary(content);
|
|
return (SetRequestContent(request, body));
|
|
}
|
|
|
|
internal void ParseLinkHeader(HttpResponseMessage response, System.Uri requestUri)
|
|
{
|
|
if (_relationLink == null)
|
|
{
|
|
// Must ignore the case of relation links. See RFC 8288 (https://tools.ietf.org/html/rfc8288)
|
|
_relationLink = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
else
|
|
{
|
|
_relationLink.Clear();
|
|
}
|
|
|
|
// we only support the URL in angle brackets and `rel`, other attributes are ignored
|
|
// user can still parse it themselves via the Headers property
|
|
const string pattern = "<(?<url>.*?)>;\\s*rel=(\"?)(?<rel>.*?)\\1[^\\w -.]?";
|
|
IEnumerable<string> links;
|
|
if (response.Headers.TryGetValues("Link", out links))
|
|
{
|
|
foreach (string linkHeader in links)
|
|
{
|
|
foreach (string link in linkHeader.Split(','))
|
|
{
|
|
Match match = Regex.Match(link, pattern);
|
|
if (match.Success)
|
|
{
|
|
string url = match.Groups["url"].Value;
|
|
string rel = match.Groups["rel"].Value;
|
|
if (url != string.Empty && rel != string.Empty && !_relationLink.ContainsKey(rel))
|
|
{
|
|
Uri absoluteUri = new(requestUri, url);
|
|
_relationLink.Add(rel, absoluteUri.AbsoluteUri);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds content to a <see cref="MultipartFormDataContent"/>. Object type detection is used to determine if the value is string, File, or Collection.
|
|
/// </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="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)
|
|
{
|
|
if (formData == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(formData));
|
|
}
|
|
|
|
// It is possible that the dictionary keys or values are PSObject wrapped depending on how the dictionary is defined and assigned.
|
|
// Before processing the field name and value we need to ensure we are working with the base objects and not the PSObject wrappers.
|
|
|
|
// Unwrap fieldName PSObjects
|
|
if (fieldName is PSObject namePSObject)
|
|
{
|
|
fieldName = namePSObject.BaseObject;
|
|
}
|
|
|
|
// Unwrap fieldValue PSObjects
|
|
if (fieldValue is PSObject valuePSObject)
|
|
{
|
|
fieldValue = valuePSObject.BaseObject;
|
|
}
|
|
|
|
// Treat a single FileInfo as a FileContent
|
|
if (fieldValue is FileInfo file)
|
|
{
|
|
formData.Add(GetMultipartFileContent(fieldName: fieldName, file: file));
|
|
return;
|
|
}
|
|
|
|
// Treat Strings and other single values as a StringContent.
|
|
// If enumeration is false, also treat IEnumerables as StringContents.
|
|
// String implements IEnumerable so the explicit check is required.
|
|
if (!enumerate || fieldValue is string || fieldValue is not IEnumerable)
|
|
{
|
|
formData.Add(GetMultipartStringContent(fieldName: fieldName, fieldValue: fieldValue));
|
|
return;
|
|
}
|
|
|
|
// Treat the value as a collection and enumerate it if enumeration is true
|
|
if (enumerate && fieldValue is IEnumerable items)
|
|
{
|
|
foreach (var item in items)
|
|
{
|
|
// Recruse, but do not enumerate the next level. IEnumerables will be treated as single values.
|
|
AddMultipartContent(fieldName: fieldName, fieldValue: item, formData: formData, enumerate: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="StringContent"/> from the supplied field name and field value. Uses <see cref="LanguagePrimitives.ConvertTo{T}(object)"/> to convert the objects to strings.
|
|
/// </summary>
|
|
/// <param name="fieldName">The Field Name to use for the <see cref="StringContent"/></param>
|
|
/// <param name="fieldValue">The Field Value to use for the <see cref="StringContent"/></param>
|
|
private static StringContent GetMultipartStringContent(object fieldName, object fieldValue)
|
|
{
|
|
var contentDisposition = new ContentDispositionHeaderValue("form-data");
|
|
// .NET does not enclose field names in quotes, however, modern browsers and curl do.
|
|
contentDisposition.Name = "\"" + LanguagePrimitives.ConvertTo<string>(fieldName) + "\"";
|
|
|
|
var result = new StringContent(LanguagePrimitives.ConvertTo<string>(fieldValue));
|
|
result.Headers.ContentDisposition = contentDisposition;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="StreamContent"/> from the supplied field name and <see cref="Stream"/>. Uses <see cref="LanguagePrimitives.ConvertTo{T}(object)"/> to convert the fieldname to a string.
|
|
/// </summary>
|
|
/// <param name="fieldName">The Field Name to use for the <see cref="StreamContent"/></param>
|
|
/// <param name="stream">The <see cref="Stream"/> to use for the <see cref="StreamContent"/></param>
|
|
private static StreamContent GetMultipartStreamContent(object fieldName, Stream stream)
|
|
{
|
|
var contentDisposition = new ContentDispositionHeaderValue("form-data");
|
|
// .NET does not enclose field names in quotes, however, modern browsers and curl do.
|
|
contentDisposition.Name = "\"" + LanguagePrimitives.ConvertTo<string>(fieldName) + "\"";
|
|
|
|
var result = new StreamContent(stream);
|
|
result.Headers.ContentDisposition = contentDisposition;
|
|
result.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="StreamContent"/> from the supplied field name and file. Calls <see cref="GetMultipartStreamContent(object, Stream)"/> to create the <see cref="StreamContent"/> and then sets the file name.
|
|
/// </summary>
|
|
/// <param name="fieldName">The Field Name to use for the <see cref="StreamContent"/></param>
|
|
/// <param name="file">The file to use for the <see cref="StreamContent"/></param>
|
|
private static StreamContent GetMultipartFileContent(object fieldName, FileInfo file)
|
|
{
|
|
var result = GetMultipartStreamContent(fieldName: fieldName, stream: new FileStream(file.FullName, FileMode.Open));
|
|
// .NET does not enclose field names in quotes, however, modern browsers and curl do.
|
|
result.Headers.ContentDisposition.FileName = "\"" + file.Name + "\"";
|
|
|
|
return result;
|
|
}
|
|
#endregion Helper Methods
|
|
}
|
|
}
|