diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index 60ee931591a..415564ffd21 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Management.Automation; @@ -95,7 +98,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// /// Cancellation token source. /// - internal CancellationTokenSource _cancelToken = null; + internal CancellationTokenSource? _cancelToken = null; /// /// Automatically follow Rel Links. @@ -120,7 +123,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// /// Automatically follow Rel Links. /// - internal Dictionary _relationLink = null; + internal Dictionary? _relationLink = null; /// /// The current size of the local file being resumed. @@ -154,7 +157,8 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// [Parameter(Position = 0, Mandatory = true)] [ValidateNotNullOrEmpty] - public virtual Uri Uri { get; set; } + [DisallowNull] + public virtual Uri Uri { get; set; } = null!; #endregion URI @@ -166,7 +170,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable [Parameter] [ArgumentToVersionTransformation] [HttpVersionCompletions] - public virtual Version HttpVersion { get; set; } + public virtual Version? HttpVersion { get; set; } #endregion HTTP Version @@ -175,14 +179,17 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// Gets or sets the Session property. /// [Parameter] - public virtual WebRequestSession WebSession { get; set; } + [ValidateNotNull] + [DisallowNull] + public virtual WebRequestSession WebSession { get; set; } = new WebRequestSession(); /// /// Gets or sets the SessionVariable property. /// [Parameter] [Alias("SV")] - public virtual string SessionVariable { get; set; } + [ValidateNotNullOrWhiteSpace] + public virtual string? SessionVariable { get; set; } #endregion Session @@ -209,7 +216,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// [Parameter] [Credential] - public virtual PSCredential Credential { get; set; } + public virtual PSCredential? Credential { get; set; } /// /// Gets or sets the UseDefaultCredentials property. @@ -222,14 +229,14 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// [Parameter] [ValidateNotNullOrEmpty] - public virtual string CertificateThumbprint { get; set; } + public virtual string? CertificateThumbprint { get; set; } /// /// Gets or sets the Certificate property. /// [Parameter] [ValidateNotNull] - public virtual X509Certificate Certificate { get; set; } + public virtual X509Certificate? Certificate { get; set; } /// /// Gets or sets the SkipCertificateCheck property. @@ -247,7 +254,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// Gets or sets the Token property. Token is required by Authentication OAuth and Bearer. /// [Parameter] - public virtual SecureString Token { get; set; } + public virtual SecureString? Token { get; set; } #endregion Authorization and Credentials @@ -257,7 +264,7 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// Gets or sets the UserAgent property. /// [Parameter] - public virtual string UserAgent { get; set; } + public virtual string? UserAgent { get; set; } /// /// Gets or sets the DisableKeepAlive property. @@ -275,9 +282,9 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable /// /// Gets or sets the Headers property. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [Parameter] - public virtual IDictionary Headers { get; set; } + public virtual IDictionary? Headers { get; set; } /// /// Gets or sets the SkipHeaderValidation property. @@ -352,14 +359,15 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable [Parameter(Mandatory = true, ParameterSetName = "CustomMethodNoProxy")] [Alias("CM")] [ValidateNotNullOrEmpty] - public virtual string CustomMethod + [DisallowNull] + public virtual string? CustomMethod { get => _custommethod; set => _custommethod = value.ToUpperInvariant(); } - private string _custommethod; + private string? _custommethod; /// /// Gets or sets the PreserveHttpMethodOnRedirect property. @@ -387,7 +395,7 @@ public virtual string CustomMethod /// [Parameter(ParameterSetName = "StandardMethod")] [Parameter(ParameterSetName = "CustomMethod")] - public virtual Uri Proxy { get; set; } + public virtual Uri? Proxy { get; set; } /// /// Gets or sets the ProxyCredential property. @@ -395,7 +403,7 @@ public virtual string CustomMethod [Parameter(ParameterSetName = "StandardMethod")] [Parameter(ParameterSetName = "CustomMethod")] [Credential] - public virtual PSCredential ProxyCredential { get; set; } + public virtual PSCredential? ProxyCredential { get; set; } /// /// Gets or sets the ProxyUseDefaultCredentials property. @@ -412,7 +420,7 @@ public virtual string CustomMethod /// Gets or sets the Body property. /// [Parameter(ValueFromPipeline = true)] - public virtual object Body { get; set; } + public virtual object? Body { get; set; } /// /// Dictionary for use with RFC-7578 multipart/form-data submissions. @@ -420,32 +428,32 @@ public virtual string CustomMethod /// A value may be a collection of form values or single form value. /// [Parameter] - public virtual IDictionary Form { get; set; } + public virtual IDictionary? Form { get; set; } /// /// Gets or sets the ContentType property. /// [Parameter] - public virtual string ContentType { get; set; } + public virtual string? ContentType { get; set; } /// /// Gets or sets the TransferEncoding property. /// [Parameter] [ValidateSet("chunked", "compress", "deflate", "gzip", "identity", IgnoreCase = true)] - public virtual string TransferEncoding { get; set; } + public virtual string? TransferEncoding { get; set; } /// /// Gets or sets the InFile property. /// [Parameter] [ValidateNotNullOrEmpty] - public virtual string InFile { get; set; } + public virtual string? InFile { get; set; } /// /// Keep the original file path after the resolved provider path is assigned to InFile. /// - private string _originalFilePath; + private string? _originalFilePath; #endregion Input @@ -456,7 +464,7 @@ public virtual string CustomMethod /// [Parameter] [ValidateNotNullOrEmpty] - public virtual string OutFile { get; set; } + public virtual string? OutFile { get; set; } /// /// Gets or sets the PassThrough property. @@ -484,6 +492,7 @@ public virtual string CustomMethod internal string QualifiedOutFile => QualifyFilePath(OutFile); + [System.Diagnostics.CodeAnalysis.AllowNull] internal string _qualifiedOutFile; internal bool ShouldCheckHttpStatus => !SkipHttpErrorCheck; @@ -550,7 +559,7 @@ protected override void ProcessRecord() FillRequestStream(request); try { - long requestContentLength = request.Content is null ? 0 : request.Content.Headers.ContentLength.Value; + long? requestContentLength = request.Content?.Headers.ContentLength.GetValueOrDefault(); string reqVerboseMsg = string.Format( CultureInfo.CurrentCulture, @@ -565,7 +574,7 @@ protected override void ProcessRecord() using HttpResponseMessage response = GetResponse(client, request, handleRedirect); - string contentType = ContentHelper.GetContentType(response); + string? contentType = ContentHelper.GetContentType(response); long? contentLength = response.Content.Headers.ContentLength; string respVerboseMsg = contentLength is null ? string.Format(CultureInfo.CurrentCulture, WebCmdletStrings.WebResponseNoSizeVerboseMsg, contentType) @@ -579,8 +588,7 @@ protected override void ProcessRecord() // 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) + && response.Content.Headers.ContentRange?.Length.GetValueOrDefault() == _resumeFileSize) { _isSuccess = true; WriteVerbose(string.Format( @@ -597,7 +605,7 @@ protected override void ProcessRecord() { // We will skip detection if either of the URIs is relative, because the 'Scheme' property is not supported on a relative URI. // If we have to skip the check, an error may be thrown later if it's actually an insecure https-to-http redirect. - bool originIsHttps = response.RequestMessage.RequestUri.IsAbsoluteUri && response.RequestMessage.RequestUri.Scheme == "https"; + bool originIsHttps = response.RequestMessage?.RequestUri is not null && response.RequestMessage.RequestUri.IsAbsoluteUri && response.RequestMessage.RequestUri.Scheme == "https"; bool destinationIsHttp = response.Headers.Location is not null && response.Headers.Location.IsAbsoluteUri && response.Headers.Location.Scheme == "http"; if (originIsHttps && destinationIsHttp) @@ -619,6 +627,7 @@ protected override void ProcessRecord() HttpResponseException httpEx = new(message, response); ErrorRecord er = new(httpEx, "WebCmdletWebResponseException", ErrorCategory.InvalidOperation, request); string detailMsg = string.Empty; + try { string error = StreamHelper.GetResponseString(response, _cancelToken.Token); @@ -672,7 +681,7 @@ protected override void ProcessRecord() _cancelToken = null; } - if (_followRelLink) + if (_followRelLink && _relationLink is not null) { if (!_relationLink.ContainsKey("next")) { @@ -714,7 +723,7 @@ protected virtual void Dispose(bool disposing) if (disposing && !IsPersistentSession()) { WebSession?.Dispose(); - WebSession = null; + WebSession = null!; } _disposed = true; @@ -737,7 +746,7 @@ public void Dispose() internal virtual void ValidateParameters() { // Sessions - if (WebSession is not null && SessionVariable is not null) + if (MyInvocation.BoundParameters.ContainsKey(nameof(WebSession)) && MyInvocation.BoundParameters.ContainsKey(nameof(SessionVariable))) { ErrorRecord error = GetValidationError(WebCmdletStrings.SessionConflict, "WebCmdletSessionConflictException"); ThrowTerminatingError(error); @@ -815,7 +824,7 @@ internal virtual void ValidateParameters() // Validate InFile path if (InFile is not null) { - ErrorRecord errorRecord = null; + ErrorRecord? errorRecord = null; try { @@ -892,9 +901,6 @@ internal virtual void ValidateParameters() internal virtual void PrepareSession() { - // Make sure we have a valid WebRequestSession object to work with - WebSession ??= new WebRequestSession(); - if (SessionVariable is not null) { // Save the session back to the PS environment if requested @@ -1000,7 +1006,7 @@ internal virtual void PrepareSession() { foreach (string key in Headers.Keys) { - object value = Headers[key]; + object? value = Headers[key]; // null is not valid value for header. // We silently ignore header if value is null. @@ -1035,9 +1041,9 @@ internal virtual HttpClient GetHttpClient(bool handleRedirect) return client; } - internal virtual HttpRequestMessage GetRequest(Uri uri) + internal virtual HttpRequestMessage GetRequest(Uri? uri) { - Uri requestUri = PrepareUri(uri); + Uri? requestUri = PrepareUri(uri); HttpMethod httpMethod = string.IsNullOrEmpty(CustomMethod) ? GetHttpMethod(Method) : new HttpMethod(CustomMethod); // Create the base WebRequest object @@ -1079,7 +1085,7 @@ internal virtual HttpRequestMessage GetRequest(Uri uri) } // Set 'User-Agent' if WebSession.Headers doesn't already contain it - if (WebSession.Headers.TryGetValue(HttpKnownHeaderNames.UserAgent, out string userAgent)) + if (WebSession.Headers.TryGetValue(HttpKnownHeaderNames.UserAgent, out string? userAgent)) { WebSession.UserAgent = userAgent; } @@ -1144,7 +1150,7 @@ internal virtual void FillRequestStream(HttpRequestMessage request) else if (request.Method == HttpMethod.Post || request.Method == HttpMethod.Put) { // Win8:545310 Invoke-WebRequest does not properly set MIME type for POST - WebSession.ContentHeaders.TryGetValue(HttpKnownHeaderNames.ContentType, out string contentType); + WebSession.ContentHeaders.TryGetValue(HttpKnownHeaderNames.ContentType, out string? contentType); if (string.IsNullOrEmpty(contentType)) { WebSession.ContentHeaders[HttpKnownHeaderNames.ContentType] = "application/x-www-form-urlencoded"; @@ -1255,12 +1261,12 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // Add 1 to account for the first request. int totalRequests = WebSession.MaximumRetryCount + 1; HttpRequestMessage currentRequest = request; - HttpResponseMessage response = null; + HttpResponseMessage? response = null; do { // Track the current URI being used by various requests and re-requests. - Uri currentUri = currentRequest.RequestUri; + Uri? currentUri = currentRequest.RequestUri; _cancelToken = new CancellationTokenSource(); response = client.SendAsync(currentRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult(); @@ -1286,7 +1292,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM CustomMethod = string.Empty; } - currentUri = new Uri(request.RequestUri, response.Headers.Location); + currentUri = request.RequestUri is null ? response.Headers.Location : new Uri(request.RequestUri, response.Headers.Location); // Continue to handle redirection using HttpRequestMessage redirectRequest = GetRequest(currentUri); @@ -1299,10 +1305,9 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // 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)) + && response.Content.Headers.ContentRange?.Length != _resumeFileSize) { - _cancelToken.Cancel(); + _cancelToken?.Cancel(); WriteVerbose(WebCmdletStrings.WebMethodResumeFailedVerboseMsg); @@ -1314,7 +1319,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM { FillRequestStream(requestWithoutRange); - long requestContentLength = requestWithoutRange.Content is null ? 0 : requestWithoutRange.Content.Headers.ContentLength.Value; + long? requestContentLength = requestWithoutRange.Content?.Headers.ContentLength.GetValueOrDefault(); string reqVerboseMsg = string.Format( CultureInfo.CurrentCulture, @@ -1339,7 +1344,7 @@ internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestM // If the status code is 429 get the retry interval from the Headers. // Ignore broken header and its value. - if (response.StatusCode is HttpStatusCode.Conflict && response.Headers.TryGetValues(HttpKnownHeaderNames.RetryAfter, out IEnumerable retryAfter)) + if (response.StatusCode is HttpStatusCode.Conflict && response.Headers.TryGetValues(HttpKnownHeaderNames.RetryAfter, out IEnumerable? retryAfter)) { try { @@ -1388,7 +1393,7 @@ internal virtual void UpdateSession(HttpResponseMessage response) #endregion Virtual Methods #region Helper Methods - private Uri PrepareUri(Uri uri) + private Uri PrepareUri(Uri? uri) { uri = CheckProtocol(uri); @@ -1416,14 +1421,12 @@ private Uri PrepareUri(Uri uri) return uri; } - private static Uri CheckProtocol(Uri uri) + private static Uri CheckProtocol(Uri? uri) { - ArgumentNullException.ThrowIfNull(uri); - - return uri.IsAbsoluteUri ? uri : new Uri("http://" + uri.OriginalString); + return uri is not null && uri.IsAbsoluteUri ? uri : new Uri("http://" + uri?.OriginalString); } - private string QualifyFilePath(string path) => PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true); + private string QualifyFilePath(string? path) => PathUtils.ResolveFilePath(filePath: path, command: this, isLiteralPath: true); private static string FormatDictionary(IDictionary content) { @@ -1437,11 +1440,11 @@ private static string FormatDictionary(IDictionary content) bodyBuilder.Append('&'); } - object value = content[key]; + object? value = content[key]; // URLEncode the key and value string encodedKey = WebUtility.UrlEncode(key); - string encodedValue = value is null ? string.Empty : WebUtility.UrlEncode(value.ToString()); + string? encodedValue = value is null ? string.Empty : WebUtility.UrlEncode(value.ToString()); bodyBuilder.Append($"{encodedKey}={encodedValue}"); } @@ -1464,8 +1467,8 @@ private ErrorRecord GetValidationError(string msg, string errorId, params object private string GetBasicAuthorizationHeader() { - string password = new NetworkCredential(string.Empty, Credential.Password).Password; - string unencoded = string.Create(CultureInfo.InvariantCulture, $"{Credential.UserName}:{password}"); + string password = new NetworkCredential(string.Empty, 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)}"); } @@ -1523,8 +1526,9 @@ internal void SetRequestContent(HttpRequestMessage request, string content) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(content); + + Encoding? encoding = null; - Encoding encoding = null; string contentType = WebSession.ContentHeaders[HttpKnownHeaderNames.ContentType]; if (contentType is not null) @@ -1560,8 +1564,8 @@ internal void SetRequestContent(HttpRequestMessage request, XmlNode xmlNode) ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(xmlNode); - byte[] bytes = null; - XmlDocument doc = xmlNode as XmlDocument; + byte[]? bytes = null; + XmlDocument? doc = xmlNode as XmlDocument; if (doc?.FirstChild is XmlDeclaration decl && !string.IsNullOrEmpty(decl.Encoding)) { Encoding encoding = Encoding.GetEncoding(decl.Encoding); @@ -1623,7 +1627,13 @@ internal void SetRequestContent(HttpRequestMessage request, IDictionary content) internal void ParseLinkHeader(HttpResponseMessage response) { - Uri requestUri = response.RequestMessage.RequestUri; + Uri? requestUri = response.RequestMessage?.RequestUri; + + if (requestUri is null) + { + return; + } + if (_relationLink is null) { // Must ignore the case of relation links. See RFC 8288 (https://tools.ietf.org/html/rfc8288) @@ -1637,7 +1647,7 @@ internal void ParseLinkHeader(HttpResponseMessage response) // 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 = "<(?.*?)>;\\s*rel=(?\")?(?(?(quoted).*?|[^,;]*))(?(quoted)\")"; - if (response.Headers.TryGetValues("Link", out IEnumerable links)) + if (response.Headers.TryGetValues("Link", out IEnumerable? links)) { foreach (string linkHeader in links) { @@ -1666,7 +1676,7 @@ internal void ParseLinkHeader(HttpResponseMessage response) /// The Field Value to use. /// The to update. /// If true, collection types in will be enumerated. If false, collections will be treated as single value. - private void AddMultipartContent(object fieldName, object fieldValue, MultipartFormDataContent formData, bool enumerate) + private void AddMultipartContent(object fieldName, object? fieldValue, MultipartFormDataContent formData, bool enumerate) { ArgumentNullException.ThrowIfNull(formData); @@ -1717,7 +1727,7 @@ private void AddMultipartContent(object fieldName, object fieldValue, MultipartF /// /// The Field Name to use for the /// The Field Value to use for the - private static StringContent GetMultipartStringContent(object fieldName, object fieldValue) + private static StringContent GetMultipartStringContent(object fieldName, object? fieldValue) { ContentDispositionHeaderValue contentDisposition = new("form-data"); @@ -1759,14 +1769,14 @@ private static StreamContent GetMultipartFileContent(object fieldName, FileInfo StreamContent 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 + "\""; + result.Headers.ContentDisposition!.FileName = "\"" + file.Name + "\""; return result; } - private static string FormatErrorMessage(string error, string contentType) + private static string FormatErrorMessage(string error, string? contentType) { - string formattedError = null; + string? formattedError = null; try { @@ -1796,9 +1806,9 @@ private static string FormatErrorMessage(string error, string contentType) } else if (ContentHelper.IsJson(contentType)) { - JsonNode jsonNode = JsonNode.Parse(error); + JsonNode? jsonNode = JsonNode.Parse(error); JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true }; - string jsonString = jsonNode.ToJsonString(options); + string jsonString = jsonNode?.ToJsonString(options) ?? throw new ArgumentNullException(); formattedError = Environment.NewLine + jsonString; } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs index 6d217cdc909..532338f3d7c 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/StreamHelper.cs @@ -451,7 +451,7 @@ internal static bool TryGetEncoding(string? characterSet, out Encoding encoding) RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.NonBacktracking ); - internal static byte[] EncodeToBytes(string str, Encoding encoding) + internal static byte[] EncodeToBytes(string str, Encoding? encoding) { // Just use the default encoding if one wasn't provided encoding ??= ContentHelper.GetDefaultEncoding();