Skip to content

Commit e8ce6a5

Browse files
authoredJul 24, 2023
Support Unix-Socket in WebCmdlets (PowerShell#19343)
1 parent 8a89fad commit e8ce6a5

File tree

10 files changed

+257
-16
lines changed

10 files changed

+257
-16
lines changed
 

‎.spelling

+1
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ ubuntu22.04
10501050
uint
10511051
un-versioned
10521052
unicode
1053+
UnixSocket
10531054
unregister-event
10541055
unregister-packagesource
10551056
unregister-psrepository

‎build.psm1

+2-1
Original file line numberDiff line numberDiff line change
@@ -1161,8 +1161,9 @@ function Publish-PSTestTools {
11611161
$tools = @(
11621162
@{ Path="${PSScriptRoot}/test/tools/TestAlc"; Output="library" }
11631163
@{ Path="${PSScriptRoot}/test/tools/TestExe"; Output="exe" }
1164-
@{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" }
11651164
@{ Path="${PSScriptRoot}/test/tools/TestService"; Output="exe" }
1165+
@{ Path="${PSScriptRoot}/test/tools/UnixSocket"; Output="exe" }
1166+
@{ Path="${PSScriptRoot}/test/tools/WebListener"; Output="exe" }
11661167
)
11671168

11681169
$Options = Get-PSOptions -DefaultToNew

‎src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs

+12-7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Net;
1212
using System.Net.Http;
1313
using System.Net.Http.Headers;
14+
using System.Net.Sockets;
1415
using System.Security;
1516
using System.Security.Authentication;
1617
using System.Security.Cryptography;
@@ -366,21 +367,23 @@ public abstract class WebRequestPSCmdlet : PSCmdlet, IDisposable
366367
[Parameter(Mandatory = true, ParameterSetName = "CustomMethodNoProxy")]
367368
[Alias("CM")]
368369
[ValidateNotNullOrEmpty]
369-
public virtual string CustomMethod
370-
{
371-
get => _custommethod;
372-
373-
set => _custommethod = value.ToUpperInvariant();
374-
}
370+
public virtual string CustomMethod { get => _customMethod; set => _customMethod = value.ToUpperInvariant(); }
375371

376-
private string _custommethod;
372+
private string _customMethod;
377373

378374
/// <summary>
379375
/// Gets or sets the PreserveHttpMethodOnRedirect property.
380376
/// </summary>
381377
[Parameter]
382378
public virtual SwitchParameter PreserveHttpMethodOnRedirect { get; set; }
383379

380+
/// <summary>
381+
/// Gets or sets the UnixSocket property.
382+
/// </summary>
383+
[Parameter]
384+
[ValidateNotNullOrEmpty]
385+
public virtual UnixDomainSocketEndPoint UnixSocket { get; set; }
386+
384387
#endregion Method
385388

386389
#region NoProxy
@@ -1019,6 +1022,8 @@ internal virtual void PrepareSession()
10191022
WebSession.MaximumRedirection = MaximumRedirection;
10201023
}
10211024

1025+
WebSession.UnixSocket = UnixSocket;
1026+
10221027
WebSession.SkipCertificateCheck = SkipCertificateCheck.IsPresent;
10231028

10241029
// Store the other supplied headers

‎src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/WebRequestSession.cs

+22-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Collections.Generic;
88
using System.Net;
99
using System.Net.Http;
10+
using System.Net.Sockets;
1011
using System.Security.Authentication;
1112
using System.Security.Cryptography.X509Certificates;
1213
using System.Threading;
@@ -33,6 +34,7 @@ public class WebRequestSession : IDisposable
3334
private bool _noProxy;
3435
private bool _disposed;
3536
private TimeSpan _connectionTimeout;
37+
private UnixDomainSocketEndPoint? _unixSocket;
3638

3739
/// <summary>
3840
/// Contains true if an existing HttpClient had to be disposed and recreated since the WebSession was last used.
@@ -144,6 +146,8 @@ public WebRequestSession()
144146

145147
internal TimeSpan ConnectionTimeout { set => SetStructVar(ref _connectionTimeout, value); }
146148

149+
internal UnixDomainSocketEndPoint UnixSocket { set => SetClassVar(ref _unixSocket, value); }
150+
147151
internal bool NoProxy
148152
{
149153
set
@@ -195,7 +199,18 @@ internal HttpClient GetHttpClient(bool suppressHttpClientRedirects, out bool cli
195199

196200
private HttpClient CreateHttpClient()
197201
{
198-
HttpClientHandler handler = new();
202+
SocketsHttpHandler handler = new();
203+
204+
if (_unixSocket is not null)
205+
{
206+
handler.ConnectCallback = async (context, token) =>
207+
{
208+
Socket socket = new(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
209+
await socket.ConnectAsync(_unixSocket).ConfigureAwait(false);
210+
211+
return new NetworkStream(socket, ownsSocket: false);
212+
};
213+
}
199214

200215
handler.CookieContainer = Cookies;
201216
handler.AutomaticDecompression = DecompressionMethods.All;
@@ -204,9 +219,9 @@ private HttpClient CreateHttpClient()
204219
{
205220
handler.Credentials = Credentials;
206221
}
207-
else
222+
else if (UseDefaultCredentials)
208223
{
209-
handler.UseDefaultCredentials = UseDefaultCredentials;
224+
handler.Credentials = CredentialCache.DefaultCredentials;
210225
}
211226

212227
if (_noProxy)
@@ -220,13 +235,12 @@ private HttpClient CreateHttpClient()
220235

221236
if (Certificates is not null)
222237
{
223-
handler.ClientCertificates.AddRange(Certificates);
238+
handler.SslOptions.ClientCertificates = new X509CertificateCollection(Certificates);
224239
}
225240

226241
if (_skipCertificateCheck)
227242
{
228-
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
229-
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
243+
handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };
230244
}
231245

232246
handler.AllowAutoRedirect = _allowAutoRedirect;
@@ -235,9 +249,9 @@ private HttpClient CreateHttpClient()
235249
handler.MaxAutomaticRedirections = MaximumRedirection;
236250
}
237251

238-
handler.SslProtocols = (SslProtocols)_sslProtocol;
252+
handler.SslOptions.EnabledSslProtocols = (SslProtocols)_sslProtocol;
239253

240-
// Check timeout setting (in seconds instead of milliseconds as in HttpWebRequest)
254+
// Check timeout setting (in seconds)
241255
return new HttpClient(handler)
242256
{
243257
Timeout = _connectionTimeout

‎test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1

+20
Original file line numberDiff line numberDiff line change
@@ -4465,6 +4465,26 @@ Describe 'Invoke-WebRequest and Invoke-RestMethod support Cancellation through C
44654465
}
44664466
}
44674467

4468+
Describe "Web cmdlets Unix Sockets tests" -Tags "CI", "RequireAdminOnWindows" {
4469+
BeforeAll {
4470+
$unixSocket = Get-UnixSocketName
4471+
$WebListener = Start-UnixSocket $unixSocket
4472+
}
4473+
4474+
It "Execute Invoke-WebRequest with -UnixSocket" {
4475+
$uri = Get-UnixSocketUri
4476+
$result = Invoke-WebRequest $uri -UnixSocket $unixSocket
4477+
$result.StatusCode | Should -Be "200"
4478+
$result.Content | Should -Be "Hello World Unix Socket."
4479+
}
4480+
4481+
It "Execute Invoke-RestMethod with -UnixSocket" {
4482+
$uri = Get-UnixSocketUri
4483+
$result = Invoke-RestMethod $uri -UnixSocket $unixSocket
4484+
$result | Should -Be "Hello World Unix Socket."
4485+
}
4486+
}
4487+
44684488
Describe 'Invoke-WebRequest and Invoke-RestMethod support OperationTimeoutSeconds' -Tags "CI", "RequireAdminOnWindows" {
44694489
BeforeAll {
44704490
$oldProgress = $ProgressPreference
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# UnixSocket Module
2+
3+
A PowerShell module for managing the UnixSocket App.
4+
5+
## Running UnixSocket
6+
7+
```powershell
8+
Import-Module .\build.psm1
9+
Publish-PSTestTools
10+
$Listener = Start-UnixSocket
11+
```
12+
13+
## Stopping UnixSocket
14+
15+
```powershell
16+
Stop-UnixSocket
17+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@{
2+
ModuleVersion = '1.0.0'
3+
GUID = '86471f04-5b94-4136-a299-caf98464a06b'
4+
Author = 'PowerShell'
5+
Description = 'An UnixSocket Server for testing purposes'
6+
RootModule = 'UnixSocket.psm1'
7+
RequiredModules = @()
8+
FunctionsToExport = @(
9+
'Get-UnixSocket'
10+
'Get-UnixSocketName'
11+
'Get-UnixSocketUri'
12+
'Start-UnixSocket'
13+
'Stop-UnixSocket'
14+
)
15+
AliasesToExport = @()
16+
CmdletsToExport = @()
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Class UnixSocket
5+
{
6+
[System.Management.Automation.Job]$Job
7+
8+
UnixSocket () { }
9+
10+
[String] GetStatus()
11+
{
12+
return $this.Job.JobStateInfo.State
13+
}
14+
}
15+
16+
[UnixSocket]$UnixSocket
17+
18+
function Get-UnixSocket
19+
{
20+
[CmdletBinding(ConfirmImpact = 'Low')]
21+
[OutputType([UnixSocket])]
22+
param()
23+
24+
process
25+
{
26+
return [UnixSocket]$Script:UnixSocket
27+
}
28+
}
29+
30+
function Start-UnixSocket
31+
{
32+
[CmdletBinding(ConfirmImpact = 'Low')]
33+
[OutputType([UnixSocket])]
34+
param([string] $socketPath)
35+
36+
process
37+
{
38+
$runningListener = Get-UnixSocket
39+
if ($null -ne $runningListener -and $runningListener.GetStatus() -eq 'Running')
40+
{
41+
return $runningListener
42+
}
43+
44+
$initTimeoutSeconds = 25
45+
$appExe = (Get-Command UnixSocket).Path
46+
$initCompleteMessage = 'Now listening on'
47+
$sleepMilliseconds = 100
48+
49+
$Job = Start-Job {
50+
$path = Split-Path -Parent (Get-Command UnixSocket).Path -Verbose
51+
Push-Location $path -Verbose
52+
'appEXE: {0}' -f $using:appExe
53+
$env:ASPNETCORE_ENVIRONMENT = 'Development'
54+
& $using:appExe $using:socketPath
55+
}
56+
57+
$Script:UnixSocket = [UnixSocket]@{
58+
Job = $Job
59+
}
60+
61+
# Count iterations of $sleepMilliseconds instead of using system time to work around possible CI VM sleep/delays
62+
$sleepCountRemaining = $initTimeoutSeconds * 1000 / $sleepMilliseconds
63+
do
64+
{
65+
Start-Sleep -Milliseconds $sleepMilliseconds
66+
$initStatus = $Job.ChildJobs[0].Output | Out-String
67+
$isRunning = $initStatus -match $initCompleteMessage
68+
$sleepCountRemaining--
69+
}
70+
while (-not $isRunning -and $sleepCountRemaining -gt 0)
71+
72+
if (-not $isRunning)
73+
{
74+
$jobErrors = $Job.ChildJobs[0].Error | Out-String
75+
$jobOutput = $Job.ChildJobs[0].Output | Out-String
76+
$jobVerbose = $Job.ChildJobs[0].Verbose | Out-String
77+
$Job | Stop-Job
78+
$Job | Remove-Job -Force
79+
$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
80+
throw $message
81+
}
82+
return $Script:UnixSocket
83+
}
84+
}
85+
86+
function Stop-UnixSocket
87+
{
88+
[CmdletBinding(ConfirmImpact = 'Low')]
89+
[OutputType([Void])]
90+
param()
91+
92+
process
93+
{
94+
$Script:UnixSocket.Job | Stop-Job -PassThru | Remove-Job
95+
$Script:UnixSocket = $null
96+
}
97+
}
98+
99+
function Get-UnixSocketName {
100+
[CmdletBinding()]
101+
[OutputType([string])]
102+
param ()
103+
104+
process {
105+
return [System.IO.Path]::Join([System.IO.Path]::GetTempPath(), [System.IO.Path]::ChangeExtension([System.IO.Path]::GetRandomFileName(), "sock"))
106+
}
107+
}
108+
109+
function Get-UnixSocketUri {
110+
[CmdletBinding()]
111+
[OutputType([Uri])]
112+
param ()
113+
114+
process {
115+
$runningListener = Get-UnixSocket
116+
if ($null -eq $runningListener -or $runningListener.GetStatus() -ne 'Running')
117+
{
118+
return $null
119+
}
120+
$Uri = [System.UriBuilder]::new()
121+
$Uri.Host = '127.0.0.0'
122+
123+
return [Uri]$Uri.ToString()
124+
}
125+
}

‎test/tools/UnixSocket/UnixSocket.cs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
using Microsoft.AspNetCore.Builder;
7+
using static Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions;
8+
9+
namespace UnixSocket
10+
{
11+
public static class Program
12+
{
13+
public static void Main(string[] args)
14+
{
15+
WebApplicationBuilder builder = WebApplication.CreateBuilder();
16+
builder.WebHost.ConfigureKestrel(options =>
17+
{
18+
options.ListenUnixSocket(args[0]);
19+
});
20+
21+
var app = builder.Build();
22+
app.MapGet("/", () => "Hello World Unix Socket.");
23+
24+
app.Run();
25+
}
26+
}
27+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<Import Project="..\..\Test.Common.props" />
4+
5+
<PropertyGroup>
6+
<Description>A very simple ASP.NET Core app to provide an UnixSocket server for testing.</Description>
7+
<AssemblyName>UnixSocket</AssemblyName>
8+
<OutputType>Exe</OutputType>
9+
<TieredCompilation>true</TieredCompilation>
10+
<TieredCompilationQuickJit>true</TieredCompilationQuickJit>
11+
<RuntimeIdentifiers>win7-x86;win7-x64;osx-x64;linux-x64</RuntimeIdentifiers>
12+
</PropertyGroup>
13+
14+
</Project>

0 commit comments

Comments
 (0)