Add -HttpVersion parameter to web cmdlets (#15853)

This commit is contained in:
Maksim Ivanyuk 2021-11-03 02:53:36 +03:00 committed by GitHub
parent 107fd7ab1f
commit 674ef888a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 329 additions and 101 deletions

View file

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Net;
using System.Reflection;
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// A completer for HTTP version names.
/// </summary>
internal sealed class HttpVersionCompletionsAttribute : ArgumentCompletionsAttribute
{
public static readonly string[] AllowedVersions;
static HttpVersionCompletionsAttribute()
{
FieldInfo[] fields = typeof(HttpVersion).GetFields(BindingFlags.Static | BindingFlags.Public);
var versions = new List<string>(fields.Length - 1);
for (int i = 0; i < fields.Length; i++)
{
// skip field Unknown and not Version type
if (fields[i].Name == nameof(HttpVersion.Unknown) || fields[i].FieldType != typeof(Version))
{
continue;
}
var version = (Version?)fields[i].GetValue(null);
if (version is not null)
{
versions.Add(version.ToString());
}
}
AllowedVersions = versions.ToArray();
}
/// <inheritdoc/>
public HttpVersionCompletionsAttribute() : base(AllowedVersions)
{
}
}
}

View file

@ -107,6 +107,18 @@ namespace Microsoft.PowerShell.Commands
#endregion
#region HTTP Version
/// <summary>
/// Gets or sets the HTTP Version property.
/// </summary>
[Parameter]
[ArgumentToVersionTransformation]
[HttpVersionCompletions]
public virtual Version HttpVersion { get; set; }
#endregion
#region Session
/// <summary>
/// Gets or sets the Session property.
@ -1081,6 +1093,11 @@ namespace Microsoft.PowerShell.Commands
// create the base WebRequest object
var request = new HttpRequestMessage(httpMethod, requestUri);
if (HttpVersion is not null)
{
request.Version = HttpVersion;
}
// pull in session data
if (WebSession.Headers.Count > 0)
{
@ -1413,6 +1430,7 @@ namespace Microsoft.PowerShell.Commands
string reqVerboseMsg = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebMethodInvocationVerboseMsg,
requestWithoutRange.Version,
requestWithoutRange.Method,
requestWithoutRange.RequestUri,
requestContentLength);
@ -1505,10 +1523,14 @@ namespace Microsoft.PowerShell.Commands
if (request.Content != null)
requestContentLength = request.Content.Headers.ContentLength.Value;
string reqVerboseMsg = string.Format(CultureInfo.CurrentCulture,
string reqVerboseMsg = string.Format(
CultureInfo.CurrentCulture,
WebCmdletStrings.WebMethodInvocationVerboseMsg,
request.Version,
request.Method,
request.RequestUri,
requestContentLength);
WriteVerbose(reqVerboseMsg);
HttpResponseMessage response = GetResponse(client, request, keepAuthorization);

View file

@ -129,7 +129,7 @@
<data name="AuthenticationCredentialNotSupplied" xml:space="preserve">
<value>The cmdlet cannot run because the following parameter is not specified: Credential. The supplied Authentication type requires a Credential. Specify Credential, then retry.</value>
</data>
<data name="AuthenticationTokenNotSupplied" xml:space="preserve">
<data name="AuthenticationTokenNotSupplied" xml:space="preserve">
<value>The cmdlet cannot run because the following parameter is not specified: Token. The supplied Authentication type requires a Token. Specify Token, then retry.</value>
</data>
<data name="AuthenticationTokenConflict" xml:space="preserve">
@ -247,7 +247,7 @@
<value>Following rel link {0}</value>
</data>
<data name="WebMethodInvocationVerboseMsg" xml:space="preserve">
<value>{0} with {1}-byte payload</value>
<value>HTTP/{0} {1} {2} with {3}-byte payload</value>
</data>
<data name="WebMethodResumeFailedVerboseMsg" xml:space="preserve">
<value>The remote server indicated it could not resume downloading. The local file will be overwritten.</value>

View file

@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System.Management.Automation
{
/// <summary>
/// To make it easier to specify a version, we add some conversions that wouldn't happen otherwise:
/// * A simple integer, i.e. 2;
/// * A string without a dot, i.e. "2".
/// </summary>
internal class ArgumentToVersionTransformationAttribute : ArgumentTransformationAttribute
{
/// <inheritdoc/>
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
object version = PSObject.Base(inputData);
if (version is string versionStr)
{
if (TryConvertFromString(versionStr, out var convertedVersion))
{
return convertedVersion;
}
if (versionStr.Contains('.'))
{
// If the string contains a '.', let the Version constructor handle the conversion.
return inputData;
}
}
if (version is double)
{
// The conversion to int below is wrong, but the usual conversions will turn
// the double into a string, so just return the original object.
return inputData;
}
if (LanguagePrimitives.TryConvertTo<int>(version, out var majorVersion))
{
return new Version(majorVersion, 0);
}
return inputData;
}
protected virtual bool TryConvertFromString(string versionString, [NotNullWhen(true)] out Version? version)
{
version = null;
return false;
}
}
}

View file

@ -6,7 +6,6 @@ using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Management.Automation;
using System.Management.Automation.Internal;
@ -15,8 +14,10 @@ using System.Management.Automation.PSTasks;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using CommonParamSet = System.Management.Automation.Internal.CommonParameters;
using Dbg = System.Management.Automation.Diagnostics;
using NotNullWhen = System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;
namespace Microsoft.PowerShell.Commands
{
@ -2647,46 +2648,19 @@ namespace Microsoft.PowerShell.Commands
private SwitchParameter _off;
/// <summary>
/// To make it easier to specify a version, we add some conversions that wouldn't happen otherwise:
/// * A simple integer, i.e. 2
/// * A string without a dot, i.e. "2"
/// * The string 'latest', which we interpret to be the current version of PowerShell.
/// Handle 'latest', which we interpret to be the current version of PowerShell.
/// </summary>
private sealed class ArgumentToVersionTransformationAttribute : ArgumentTransformationAttribute
private sealed class ArgumentToPSVersionTransformationAttribute : ArgumentToVersionTransformationAttribute
{
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
protected override bool TryConvertFromString(string versionString, [NotNullWhen(true)] out Version version)
{
object version = PSObject.Base(inputData);
string versionStr = version as string;
if (versionStr != null)
if (string.Equals("latest", versionString, StringComparison.OrdinalIgnoreCase))
{
if (versionStr.Equals("latest", StringComparison.OrdinalIgnoreCase))
{
return PSVersionInfo.PSVersion;
}
if (versionStr.Contains('.'))
{
// If the string contains a '.', let the Version constructor handle the conversion.
return inputData;
}
version = PSVersionInfo.PSVersion;
return true;
}
if (version is double)
{
// The conversion to int below is wrong, but the usual conversions will turn
// the double into a string, so just return the original object.
return inputData;
}
int majorVersion;
if (LanguagePrimitives.TryConvertTo<int>(version, out majorVersion))
{
return new Version(majorVersion, 0);
}
return inputData;
return base.TryConvertFromString(versionString, out version);
}
}
@ -2711,7 +2685,7 @@ namespace Microsoft.PowerShell.Commands
/// Gets or sets strict mode in the current scope.
/// </summary>
[Parameter(ParameterSetName = "Version", Mandatory = true)]
[ArgumentToVersionTransformation]
[ArgumentToPSVersionTransformation]
[ValidateVersion]
[Alias("v")]
public Version Version

View file

@ -1134,6 +1134,27 @@ dir -Recurse `
$res.CompletionMatches | Should -HaveCount 4
[string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "-Path,-PipelineVariable,-PSPath,-pv"
}
It "Test completion for HttpVersion parameter name" {
$inputStr = 'Invoke-WebRequest -HttpV'
$res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length
$res.CompletionMatches | Should -HaveCount 1
$res.CompletionMatches[0].CompletionText | Should -BeExactly "-HttpVersion"
}
It "Test completion for HttpVersion parameter" {
$inputStr = 'Invoke-WebRequest -HttpVersion '
$res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length
$res.CompletionMatches | Should -HaveCount 4
[string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1,2.0,3.0"
}
It "Test completion for HttpVersion parameter with input" {
$inputStr = 'Invoke-WebRequest -HttpVersion 1'
$res = TabExpansion2 -inputScript $inputStr -cursorColumn $inputStr.Length
$res.CompletionMatches | Should -HaveCount 2
[string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1"
}
}
Context "Module completion for 'using module'" {

View file

@ -469,6 +469,23 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" {
$result.Output.Headers.Connection | Should -Be "Close"
}
It "Validate Invoke-WebRequest -HttpVersion '<httpVersion>'" -Skip:(!$IsWindows) -TestCases @(
@{ httpVersion = '1.1'},
@{ httpVersion = '2'}
) {
param($httpVersion)
# Operation options
$uri = Get-WebListenerUrl -Test 'Get' -Https
$command = "Invoke-WebRequest -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck"
$result = ExecuteWebCommand -command $command
ValidateResponse -response $result
# Validate response content
$jsonContent = $result.Output.Content | ConvertFrom-Json
$jsonContent.protocol | Should -Be "HTTP/$httpVersion"
}
It "Validate Invoke-WebRequest -MaximumRedirection" {
$uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3'
$command = "Invoke-WebRequest -Uri '$uri' -MaximumRedirection 4"
@ -2118,6 +2135,21 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" {
$result.Output.Headers.Connection | Should -Be "Close"
}
It "Validate Invoke-RestMethod -HttpVersion '<httpVersion>'" -Skip:(!$IsWindows) -TestCases @(
@{ httpVersion = '1.1'},
@{ httpVersion = '2'}
) {
param($httpVersion)
# Operation options
$uri = Get-WebListenerUrl -Test 'Get' -Https
$command = "Invoke-RestMethod -Uri $uri -HttpVersion $httpVersion -SkipCertificateCheck"
$result = ExecuteWebCommand -command $command
# Validate response
$result.Output.protocol | Should -Be "HTTP/$httpVersion"
}
It "Validate Invoke-RestMethod -MaximumRedirection" {
$uri = Get-WebListenerUrl -Test 'Redirect' -TestValue '3'
$command = "Invoke-RestMethod -Uri '$uri' -MaximumRedirection 4"

View file

@ -1,13 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Extensions;
using mvc.Models;
@ -32,12 +28,13 @@ namespace mvc.Controllers
Hashtable output = new Hashtable
{
{"args", args},
{"headers", headers},
{"origin", Request.HttpContext.Connection.RemoteIpAddress.ToString()},
{"url", UriHelper.GetDisplayUrl(Request)},
{"query", Request.QueryString.ToUriComponent()},
{"method", Request.Method}
{ "args", args },
{ "headers", headers },
{ "origin", Request.HttpContext.Connection.RemoteIpAddress.ToString() },
{ "url", UriHelper.GetDisplayUrl(Request) },
{ "query", Request.QueryString.ToUriComponent() },
{ "method", Request.Method },
{ "protocol", Request.Protocol }
};
if (Request.HasFormContentType)

View file

@ -2,18 +2,13 @@
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace mvc
{
@ -34,52 +29,79 @@ namespace mvc
WebHost.CreateDefaultBuilder()
.UseStartup<Startup>().UseKestrel(options =>
{
options.AllowSynchronousIO = true;
options.Listen(IPAddress.Loopback, int.Parse(args[2]));
options.Listen(IPAddress.Loopback, int.Parse(args[3]), listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls12;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
});
options.Listen(IPAddress.Loopback, int.Parse(args[4]), listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls11;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
});
options.Listen(IPAddress.Loopback, int.Parse(args[5]), listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
});
options.Listen(IPAddress.Loopback, int.Parse(args[6]), listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls13;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
});
options.AllowSynchronousIO = true;
options.Listen(
IPAddress.Loopback,
int.Parse(args[2]),
listenOptions =>
{
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
options.Listen(
IPAddress.Loopback,
int.Parse(args[3]),
listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls12;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
options.Listen(
IPAddress.Loopback,
int.Parse(args[4]),
listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls11;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
options.Listen(
IPAddress.Loopback,
int.Parse(args[5]),
listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
options.Listen(
IPAddress.Loopback,
int.Parse(args[6]),
listenOptions =>
{
var certificate = new X509Certificate2(args[0], args[1]);
HttpsConnectionAdapterOptions httpsOption = new HttpsConnectionAdapterOptions();
httpsOption.SslProtocols = SslProtocols.Tls13;
httpsOption.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
httpsOption.ClientCertificateValidation = (inCertificate, inChain, inPolicy) => { return true; };
httpsOption.CheckCertificateRevocation = false;
httpsOption.ServerCertificate = certificate;
listenOptions.UseHttps(httpsOption);
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
});
})
.Build();
}

View file

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Management.Automation;
using Xunit;
namespace PSTests.Parallel
{
public class ArgumentToVersionTransformationAttributeTests
{
[Theory]
[MemberData(nameof(TestCases))]
public void TestConversion(object inputData, object expected)
{
var transformation = new ArgumentToVersionTransformationAttribute();
var result = transformation.Transform(default, inputData);
Assert.Equal(expected, result);
}
public static IEnumerable<object[]> TestCases()
{
// strings
yield return new object[] { "1.1", "1.1" };
yield return new object[] { "1", new Version(1, 0) };
yield return new object[] { string.Empty, new Version(0, 0) };
// doubles
yield return new object[] { 1.0, 1.0 };
yield return new object[] { 1.1, 1.1 };
// ints
yield return new object[] { 1, new Version(1, 0) };
yield return new object[] { 2, new Version(2, 0) };
// PSObjects
yield return new object[] { new PSObject(obj: 0), new Version(0, 0) };
yield return new object[] { new PSObject(obj: 1), new Version(1, 0) };
// unhandled
var obj = new object();
yield return new object[] { obj, obj };
var date = DateTimeOffset.UtcNow;
yield return new object[] { date, date };
}
}
}