diff --git a/.spelling b/.spelling index e1499c240d..dbc54bbc39 100644 --- a/.spelling +++ b/.spelling @@ -1050,6 +1050,7 @@ ubuntu22.04 uint un-versioned unicode +UnixSocket unregister-event unregister-packagesource unregister-psrepository diff --git a/build.psm1 b/build.psm1 index bb078bfed4..228d22c191 100644 --- a/build.psm1 +++ b/build.psm1 @@ -1161,8 +1161,9 @@ function Publish-PSTestTools { $tools = @( @{ Path="${PSScriptRoot}/test/tools/TestAlc"; Output="library" } @{ Path="${PSScriptRoot}/test/tools/TestExe"; Output="exe" } - @{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" } @{ Path="${PSScriptRoot}/test/tools/TestService"; Output="exe" } + @{ Path="${PSScriptRoot}/test/tools/UnixSocket"; Output="exe" } + @{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" } ) $Options = Get-PSOptions -DefaultToNew 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 ba7d66bb9a..f1f003f37d 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 @@ -11,6 +11,7 @@ using System.Management.Automation; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Sockets; using System.Security; using System.Security.Authentication; using System.Security.Cryptography; @@ -366,14 +367,9 @@ namespace Microsoft.PowerShell.Commands [Parameter(Mandatory = true, ParameterSetName = "CustomMethodNoProxy")] [Alias("CM")] [ValidateNotNullOrEmpty] - public virtual string CustomMethod - { - get => _custommethod; + public virtual string CustomMethod { get => _customMethod; set => _customMethod = value.ToUpperInvariant(); } - set => _custommethod = value.ToUpperInvariant(); - } - - private string _custommethod; + private string _customMethod; /// /// Gets or sets the PreserveHttpMethodOnRedirect property. @@ -381,6 +377,13 @@ namespace Microsoft.PowerShell.Commands [Parameter] public virtual SwitchParameter PreserveHttpMethodOnRedirect { get; set; } + /// + /// Gets or sets the UnixSocket property. + /// + [Parameter] + [ValidateNotNullOrEmpty] + public virtual UnixDomainSocketEndPoint UnixSocket { get; set; } + #endregion Method #region NoProxy @@ -1019,6 +1022,8 @@ namespace Microsoft.PowerShell.Commands WebSession.MaximumRedirection = MaximumRedirection; } + WebSession.UnixSocket = UnixSocket; + WebSession.SkipCertificateCheck = SkipCertificateCheck.IsPresent; // Store the other supplied headers diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs index a55a7dde38..6c42ae3a4f 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.Sockets; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -33,6 +34,7 @@ namespace Microsoft.PowerShell.Commands private bool _noProxy; private bool _disposed; private TimeSpan _connectionTimeout; + private UnixDomainSocketEndPoint? _unixSocket; /// /// Contains true if an existing HttpClient had to be disposed and recreated since the WebSession was last used. @@ -144,6 +146,8 @@ namespace Microsoft.PowerShell.Commands internal TimeSpan ConnectionTimeout { set => SetStructVar(ref _connectionTimeout, value); } + internal UnixDomainSocketEndPoint UnixSocket { set => SetClassVar(ref _unixSocket, value); } + internal bool NoProxy { set @@ -195,7 +199,18 @@ namespace Microsoft.PowerShell.Commands private HttpClient CreateHttpClient() { - HttpClientHandler handler = new(); + SocketsHttpHandler handler = new(); + + if (_unixSocket is not null) + { + handler.ConnectCallback = async (context, token) => + { + Socket socket = new(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); + await socket.ConnectAsync(_unixSocket).ConfigureAwait(false); + + return new NetworkStream(socket, ownsSocket: false); + }; + } handler.CookieContainer = Cookies; handler.AutomaticDecompression = DecompressionMethods.All; @@ -204,9 +219,9 @@ namespace Microsoft.PowerShell.Commands { handler.Credentials = Credentials; } - else + else if (UseDefaultCredentials) { - handler.UseDefaultCredentials = UseDefaultCredentials; + handler.Credentials = CredentialCache.DefaultCredentials; } if (_noProxy) @@ -220,13 +235,12 @@ namespace Microsoft.PowerShell.Commands if (Certificates is not null) { - handler.ClientCertificates.AddRange(Certificates); + handler.SslOptions.ClientCertificates = new X509CertificateCollection(Certificates); } if (_skipCertificateCheck) { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - handler.ClientCertificateOptions = ClientCertificateOption.Manual; + handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } handler.AllowAutoRedirect = _allowAutoRedirect; @@ -235,9 +249,9 @@ namespace Microsoft.PowerShell.Commands handler.MaxAutomaticRedirections = MaximumRedirection; } - handler.SslProtocols = (SslProtocols)_sslProtocol; + handler.SslOptions.EnabledSslProtocols = (SslProtocols)_sslProtocol; - // Check timeout setting (in seconds instead of milliseconds as in HttpWebRequest) + // Check timeout setting (in seconds) return new HttpClient(handler) { Timeout = _connectionTimeout diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index abcfab8e8d..dee08f4e35 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -4465,6 +4465,26 @@ Describe 'Invoke-WebRequest and Invoke-RestMethod support Cancellation through C } } +Describe "Web cmdlets Unix Sockets tests" -Tags "CI", "RequireAdminOnWindows" { + BeforeAll { + $unixSocket = Get-UnixSocketName + $WebListener = Start-UnixSocket $unixSocket + } + + It "Execute Invoke-WebRequest with -UnixSocket" { + $uri = Get-UnixSocketUri + $result = Invoke-WebRequest $uri -UnixSocket $unixSocket + $result.StatusCode | Should -Be "200" + $result.Content | Should -Be "Hello World Unix Socket." + } + + It "Execute Invoke-RestMethod with -UnixSocket" { + $uri = Get-UnixSocketUri + $result = Invoke-RestMethod $uri -UnixSocket $unixSocket + $result | Should -Be "Hello World Unix Socket." + } +} + Describe 'Invoke-WebRequest and Invoke-RestMethod support OperationTimeoutSeconds' -Tags "CI", "RequireAdminOnWindows" { BeforeAll { $oldProgress = $ProgressPreference diff --git a/test/tools/Modules/UnixSocket/README.md b/test/tools/Modules/UnixSocket/README.md new file mode 100644 index 0000000000..af070f6449 --- /dev/null +++ b/test/tools/Modules/UnixSocket/README.md @@ -0,0 +1,17 @@ +# UnixSocket Module + +A PowerShell module for managing the UnixSocket App. + +## Running UnixSocket + +```powershell +Import-Module .\build.psm1 +Publish-PSTestTools +$Listener = Start-UnixSocket +``` + +## Stopping UnixSocket + +```powershell +Stop-UnixSocket +``` diff --git a/test/tools/Modules/UnixSocket/UnixSocket.psd1 b/test/tools/Modules/UnixSocket/UnixSocket.psd1 new file mode 100644 index 0000000000..f1cb3a9221 --- /dev/null +++ b/test/tools/Modules/UnixSocket/UnixSocket.psd1 @@ -0,0 +1,17 @@ +@{ + ModuleVersion = '1.0.0' + GUID = '86471f04-5b94-4136-a299-caf98464a06b' + Author = 'PowerShell' + Description = 'An UnixSocket Server for testing purposes' + RootModule = 'UnixSocket.psm1' + RequiredModules = @() + FunctionsToExport = @( + 'Get-UnixSocket' + 'Get-UnixSocketName' + 'Get-UnixSocketUri' + 'Start-UnixSocket' + 'Stop-UnixSocket' + ) + AliasesToExport = @() + CmdletsToExport = @() +} diff --git a/test/tools/Modules/UnixSocket/UnixSocket.psm1 b/test/tools/Modules/UnixSocket/UnixSocket.psm1 new file mode 100644 index 0000000000..ffd2de42fa --- /dev/null +++ b/test/tools/Modules/UnixSocket/UnixSocket.psm1 @@ -0,0 +1,125 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Class UnixSocket +{ + [System.Management.Automation.Job]$Job + + UnixSocket () { } + + [String] GetStatus() + { + return $this.Job.JobStateInfo.State + } +} + +[UnixSocket]$UnixSocket + +function Get-UnixSocket +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([UnixSocket])] + param() + + process + { + return [UnixSocket]$Script:UnixSocket + } +} + +function Start-UnixSocket +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([UnixSocket])] + param([string] $socketPath) + + process + { + $runningListener = Get-UnixSocket + if ($null -ne $runningListener -and $runningListener.GetStatus() -eq 'Running') + { + return $runningListener + } + + $initTimeoutSeconds = 25 + $appExe = (Get-Command UnixSocket).Path + $initCompleteMessage = 'Now listening on' + $sleepMilliseconds = 100 + + $Job = Start-Job { + $path = Split-Path -Parent (Get-Command UnixSocket).Path -Verbose + Push-Location $path -Verbose + 'appEXE: {0}' -f $using:appExe + $env:ASPNETCORE_ENVIRONMENT = 'Development' + & $using:appExe $using:socketPath + } + + $Script:UnixSocket = [UnixSocket]@{ + Job = $Job + } + + # Count iterations of $sleepMilliseconds instead of using system time to work around possible CI VM sleep/delays + $sleepCountRemaining = $initTimeoutSeconds * 1000 / $sleepMilliseconds + do + { + Start-Sleep -Milliseconds $sleepMilliseconds + $initStatus = $Job.ChildJobs[0].Output | Out-String + $isRunning = $initStatus -match $initCompleteMessage + $sleepCountRemaining-- + } + while (-not $isRunning -and $sleepCountRemaining -gt 0) + + if (-not $isRunning) + { + $jobErrors = $Job.ChildJobs[0].Error | Out-String + $jobOutput = $Job.ChildJobs[0].Output | Out-String + $jobVerbose = $Job.ChildJobs[0].Verbose | Out-String + $Job | Stop-Job + $Job | Remove-Job -Force + $message = 'UnixSocket did not start before the timeout was reached.{0}Errors:{0}{1}{0}Output:{0}{2}{0}Verbose:{0}{3}' -f ([System.Environment]::NewLine), $jobErrors, $jobOutput, $jobVerbose + throw $message + } + return $Script:UnixSocket + } +} + +function Stop-UnixSocket +{ + [CmdletBinding(ConfirmImpact = 'Low')] + [OutputType([Void])] + param() + + process + { + $Script:UnixSocket.Job | Stop-Job -PassThru | Remove-Job + $Script:UnixSocket = $null + } +} + +function Get-UnixSocketName { + [CmdletBinding()] + [OutputType([string])] + param () + + process { + return [System.IO.Path]::Join([System.IO.Path]::GetTempPath(), [System.IO.Path]::ChangeExtension([System.IO.Path]::GetRandomFileName(), "sock")) + } +} + +function Get-UnixSocketUri { + [CmdletBinding()] + [OutputType([Uri])] + param () + + process { + $runningListener = Get-UnixSocket + if ($null -eq $runningListener -or $runningListener.GetStatus() -ne 'Running') + { + return $null + } + $Uri = [System.UriBuilder]::new() + $Uri.Host = '127.0.0.0' + + return [Uri]$Uri.ToString() + } +} diff --git a/test/tools/UnixSocket/UnixSocket.cs b/test/tools/UnixSocket/UnixSocket.cs new file mode 100644 index 0000000000..b5bd3ed4b5 --- /dev/null +++ b/test/tools/UnixSocket/UnixSocket.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions; + +namespace UnixSocket +{ + public static class Program + { + public static void Main(string[] args) + { + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.ConfigureKestrel(options => + { + options.ListenUnixSocket(args[0]); + }); + + var app = builder.Build(); + app.MapGet("/", () => "Hello World Unix Socket."); + + app.Run(); + } + } +} diff --git a/test/tools/UnixSocket/UnixSocket.csproj b/test/tools/UnixSocket/UnixSocket.csproj new file mode 100644 index 0000000000..a4143270c8 --- /dev/null +++ b/test/tools/UnixSocket/UnixSocket.csproj @@ -0,0 +1,14 @@ + + + + + + A very simple ASP.NET Core app to provide an UnixSocket server for testing. + UnixSocket + Exe + true + true + win7-x86;win7-x64;osx-x64;linux-x64 + + +