Add-Type -AssemblyName System.Net.Http
Add-Type -AssemblyName System.Net.Security
Add-Type -AssemblyName System.Security.Cryptography.X509Certificates
$targetUrls = @{
"https://outlook.com" = $null
"https://google.com" = $null
}
function Invoke-ServerCertificateCusomValidationCallback {
<#
.SYNOPSIS
Call-back function for System.Net.Http.HttpClientHandler.ServerCertificateCustomValidationCallback where this script gets access to the HTTPs Requests certificate data.
#>
[CmdletBinding()]
param (
[System.Net.Http.HttpRequestMessage]$httpRequestMsg,
[System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
[System.Security.Cryptography.X509Certificates.X509Chain]$certificateChain,
[System.Net.Security.SslPolicyErrors]$sslErrors
)
process {
if ($targetUrls.ContainsKey($httpRequestMsg.RequestUri.ToString())) {
$expirationTimeDelta = (
[DateTime]::Parse($certificate.GetExpirationDateString()) -
[DateTime]::Today
).Days
Write-Host "\t${$httpRequestMsg.RequestUri} expires in ${$expirationTimeDelta} days!"
}
}
end {
return $sslErrors -eq [System.Net.Security.SslPolicyErrors].None
}
}
# Set-up HttpClient & handler
$handler = New-Object -TypeName System.Net.Http.HttpClientHandler
$handler.ServerCertificateCustomValidationCallback = Invoke-ServerCertificateCusomValidationCallback
$client = New-Object -TypeName System.Net.Http.HttpClient -ArgumentList $handler
# Fetch HTTP Requests Async
$targetUrls.Keys.Clone() | ForEach-Object {
Write-Host "Initating HTTPs Request to ${$_} ..."
$targetUrls[$_] = $client.GetAsync($_).GetAwaiter()
}
# Await Async HTTP Requests to return
$targetUrls.Keys.Clone() | ForEach-Object {
# These two for-loops does NOT leverage the async functionality of the HTTP requests...
$targetUrls[$_].GetResult()
Write-Host "${$_} returned status code ${$targetUrls[$_].StatusCode}"
}
Initiating HTTPs Request to https://outlook.com ...
Initiating HTTPs Request to https://google.com ...
https://google.com expires in X days!
https://outlook.com expires in X days!
https://outlook.com returned status code 200
https://google.com returned status code 200
When copy-pasting line-by-line into a pwsh terminal, I get the below error when copy-pasting the line;
$handler.ServerCertificateCustomValidationCallback = Invoke-ServerCertificateCusomValidationCallback
PS /home/x10an14> $handler = New-Object -TypeName System.Net.Http.HttpClientHandler
PS /home/x10an14> $handler.ServerCertificateCustomValidationCallback = Invoke-ServerCertificateCusomValidationCallback
InvalidOperation:
Line |
14 | if ($targetUrls.ContainsKey($httpRequestMsg.RequestUri.ToString())) {
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| You cannot call a method on a null-valued expression.
SetValueInvocationException: Exception setting "ServerCertificateCustomValidationCallback": "Cannot convert value "True" to type "System.Func`5[System.Net.Http.HttpRequestMessage,System.Security.Cryptography.X509Certificates.X509Certificate2,System.Security.Cryptography.X509Certificates.X509Chain,System.Net.Security.SslPolicyErrors,System.Boolean]". Error: "Invalid cast from 'System.Boolean' to 'System.Func`5[[System.Net.Http.HttpRequestMessage, System.Net.Http, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Security.Cryptography.X509Certificates.X509Certificate2, System.Security.Cryptography.X509Certificates, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Security.Cryptography.X509Certificates.X509Chain, System.Security.Cryptography.X509Certificates, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Net.Security.SslPolicyErrors, System.Net.Primitives, Version=4.1.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Boolean, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]'.""
PS /home/x10an14/Documents/bos/microsoft_csharp_certificates_test>
PS /home/x10an14> $PSVersionTable
Name Value
---- -----
PSVersion 7.0.2
PSEdition Core
GitCommitId 7.0.2
OS Linux 4.19.0-9-amd64 #1 SMP Debian 4.19.118-2+deb10u1 (2020-06-07)
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
The below C# code works, with the output given at the end;
using System;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace microsoft_csharp_certificates_test
{
class Program
{
private static string TargetUrl = "https://outlook.com";
static void Main(string[] args)
{
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = CustomCallback;
var client = new HttpClient(handler);
HttpResponseMessage response = client.GetAsync(TargetUrl).GetAwaiter().GetResult();
Console.WriteLine(TargetUrl + " response; " + response.StatusCode);
Console.WriteLine(TargetUrl + " response status code; " + (int)response.StatusCode);
}
private static bool CustomCallback(HttpRequestMessage httpRequest, X509Certificate2 certificate, X509Chain arg3, SslPolicyErrors arg4)
{
Uri UriUrlMatch = new Uri(TargetUrl);
if (UriUrlMatch == httpRequest.RequestUri) {
Console.WriteLine("Certificate received for; " + httpRequest.RequestUri);
Console.WriteLine("\tCertificate issuer; " + certificate.Issuer);
Console.WriteLine("\tCertificate subject; " + certificate.Subject);
Console.WriteLine("\tCertificate effective date string; " + certificate.GetEffectiveDateString());
Console.WriteLine("\tCertificate expiration date string; " + certificate.GetExpirationDateString());
var expirationTimeDelta = DateTime.Parse(certificate.GetExpirationDateString()) - DateTime.Today;
Console.WriteLine("\t\tDays until expiration; " + expirationTimeDelta.Days);
}
return arg4 == SslPolicyErrors.None;
}
}
}
-> $ dotnet run
Certificate received for; https://outlook.com/
Certificate issuer; CN=DigiCert Cloud Services CA-1, O=DigiCert Inc, C=US
Certificate subject; CN=outlook.com, O=Microsoft Corporation, L=Redmond, S=Washington, C=US
Certificate effective date string; 18/11/2018 01:00:00
Certificate expiration date string; 18/11/2020 13:00:00
Days until expiration; 141
https://outlook.com response; OK
https://outlook.com response status code; 200
The purpose of this script is to have;
When you assign by calling the function itself, you actually are _calling_ (invoking) the function by doing so; you're not assigning a delegate like C# does, you're just assigning the resultant value.
PowerShell _does_ generally have the capability (in some cases) to cast scriptblocks to Func<,> types, for which you'll need to target the scriptblock itself:
$handler.ServerCertificateCustomValidationCallback = ${function:Invoke-ServerCertificateCusomValidationCallback}
You also have the option of simply saving that scriptblock to a variable or directly assigning it rather than creating a named function if you prefer.
$handler.ServerCertificateCustomValidationCallback = {
[CmdletBinding()]
param (
[System.Net.Http.HttpRequestMessage]$httpRequestMsg,
[System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate,
[System.Security.Cryptography.X509Certificates.X509Chain]$certificateChain,
[System.Net.Security.SslPolicyErrors]$sslErrors
)
process {
if ($targetUrls.ContainsKey($httpRequestMsg.RequestUri.ToString())) {
$expirationTimeDelta = (
[DateTime]::Parse($certificate.GetExpirationDateString()) -
[DateTime]::Today
).Days
Write-Host "\t${$httpRequestMsg.RequestUri} expires in ${$expirationTimeDelta} days!"
}
}
end {
return $sslErrors -eq [System.Net.Security.SslPolicyErrors].None
}
}
I haven't tested this, though, so perhaps you may find the built in conversions for this kind of thing insufficient to handle it.
Adding to what @vexx32 said, keep in mind that if the callback is invoked on a different thread the results can be unpredictable.
Given the caveats you've both mentioned @SeeminglyScience and @vexx32, what's your recommended way of achieving my goals?
Given the goals listed here; https://github.com/PowerShell/PowerShell/issues/13061#issuecomment-651756023
Am I still on what you (and/or others) would suggest is the recommended path/track?
I just attempted with the suggested edit by @vexx32; https://github.com/PowerShell/PowerShell/issues/13061#issuecomment-651765614.
I think that solved the originally reported issue, but I triggered a new issue;
PS /home/x10an14> $client = New-Object -TypeName System.Net.Http.HttpClient -ArgumentList $handler
PS /home/x10an14>
PS /home/x10an14> # Fetch HTTP Requests Async
PS /home/x10an14> $targetUrls.Keys.Clone() | ForEach-Object {
>> $targetUrls[$_] = $client.GetAsync($_).GetAwaiter()
>> }
PS /home/x10an14>
PS /home/x10an14> # Await Async HTTP Requests to return
PS /home/x10an14> $targetUrls.Keys.Clone() | ForEach-Object {
>> # These two for-loops does NOT leverage the async functionality of the HTTP requests...
>> $targetUrls[$_].GetResult()
>> Write-Host "${$_} returned status code ${$targetUrls[$_].StatusCode}"
>> }
MethodInvocationException:
Line |
3 | $targetUrls[$_].GetResult()
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
returned status code
MethodInvocationException:
Line |
3 | $targetUrls[$_].GetResult()
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
returned status code
It even happened when I made an attempt by-passing the for-loops/dictonary;
PS /home/x10an14> $a = $client.GetAsync("https://outlook.com").GetAwaiter()
PS /home/x10an14> $a.GetResult()
MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
PS /home/x10an14> $client.GetAsync("https://outlook.com").GetAwaiter().GetResult()
MethodInvocationException: Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
PS /home/x10an14>
Anyone have any idea/suggestions?
Hard to say without seeing the exception details; if you call Get-Error you should see some more relevant detail.
Ho damn, that's a long output.
Here goes;
PS /home/x10an14> Get-Error
Exception :
Type : System.Management.Automation.MethodInvocationException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
HResult : -2146233087
CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : HttpRequestException
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : -1
Line : $client.GetAsync("https://outlook.com").GetAwaiter().GetResult()
PositionMessage : At line:1 char:1
+ $client.GetAsync("https://outlook.com").GetAwaiter().GetResult()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
TargetSite :
Name : ConvertToMethodInvocationException
DeclaringType : System.Management.Automation.ExceptionHandlingOps, System.Management.Automation, Version=7.0.2.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35
MemberType : Method
Module : System.Management.Automation.dll
StackTrace :
at System.Management.Automation.ExceptionHandlingOps.ConvertToMethodInvocationException(Exception exception, Type typeToThrow, String methodName, Int32
numArgs, MemberInfo memberInfo)
at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at System.Management.Automation.Interpreter.DynamicInstruction`2.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Message : Exception calling "GetResult" with "0" argument(s): "The SSL connection could not be established, see inner exception."
Data : System.Collections.ListDictionaryInternal
InnerException :
Type : System.Net.Http.HttpRequestException
TargetSite :
Name : MoveNext
DeclaringType : System.Net.Http.ConnectHelper+<EstablishSslConnectionAsyncCore>d__4, System.Net.Http, Version=4.2.2.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a
MemberType : Method
Module : System.Net.Http.dll
StackTrace :
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken
cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at CallSite.Target(Closure , CallSite , Object )
Message : The SSL connection could not be established, see inner exception.
InnerException :
Type : System.Management.Automation.PSInvalidOperationException
ErrorRecord :
Exception :
Type : System.Management.Automation.ParentContainsErrorRecordException
Message : There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the
System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was:
[CmdletBinding()]
鈥icyErrors].None
}
HResult : -2146233087
CategoryInfo : InvalidOperation: (:) [], ParentContainsErrorRecordException
FullyQualifiedErrorId : ScriptBlockDelegateInvokedFromWrongThread
TargetSite :
Name : GetContextFromTLS
DeclaringType : scriptblock
MemberType : Method
Module : System.Management.Automation.dll
StackTrace :
at System.Management.Automation.ScriptBlock.GetContextFromTLS()
at System.Management.Automation.ScriptBlock.InvokeAsDelegateHelper(Object dollarUnder, Object dollarThis, Object[] args)
at lambda_method(Closure , HttpRequestMessage , X509Certificate2 , X509Chain , SslPolicyErrors )
at System.Net.Http.ConnectHelper.<>c__DisplayClass3_0.<EstablishSslConnectionAsync>b__0(Object sender, X509Certificate certificate, X509Chain chain,
SslPolicyErrors sslPolicyErrors)
at System.Net.Security.SslStream.UserCertValidationCallbackWrapper(String hostName, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors
sslPolicyErrors)
at System.Net.Security.SecureChannel.VerifyRemoteCertificate(RemoteCertValidationCallback remoteCertValidationCallback, ProtocolToken& alertToken)
at System.Net.Security.SslStream.CompleteHandshake(ProtocolToken& alertToken)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
at System.Net.Security.SslStream.PartialFrameCallback(AsyncProtocolRequest asyncRequest)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Security.SslStream.ThrowIfExceptional()
at System.Net.Security.SslStream.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
at System.Net.Security.SslStream.EndProcessAuthentication(IAsyncResult result)
at System.Net.Security.SslStream.EndAuthenticateAsClient(IAsyncResult asyncResult)
at System.Net.Security.SslStream.<>c.<AuthenticateAsClientAsync>b__65_1(IAsyncResult iar)
at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean
requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
at System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken
cancellationToken)
Message : There is no Runspace available to run scripts in this thread. You can provide one in the DefaultRunspace property of the
System.Management.Automation.Runspaces.Runspace type. The script block you attempted to invoke was:
[CmdletBinding()]
鈥icyErrors].None
}
Source : System.Management.Automation
HResult : -2146233079
Source : System.Net.Http
HResult : -2146233079
Source : System.Management.Automation
HResult : -2146233087
CategoryInfo : NotSpecified: (:) [], MethodInvocationException
FullyQualifiedErrorId : HttpRequestException
InvocationInfo :
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : -1
Line : $client.GetAsync("https://outlook.com").GetAwaiter().GetResult()
PositionMessage : At line:1 char:1
+ $client.GetAsync("https://outlook.com").GetAwaiter().GetResult()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 1
PS /home/x10an14>
Yeah, that's one of the possible outcomes when invoking a scriptblock delegate in a different thread. Here are your options:
Add-Type to create a compiled static method that you then convert to a delegateSystem.Linq.Expressions.ExpressionThe first is probably the most simple and portable.
@SeeminglyScience Thanks for your suggestions! Much appreciated!
I'm struggling though to see how I could leverage https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-7 (in general - I feel a little out of my depth here).
Especially without losing the filtering I do in C# with the TargetUrl class member. Without it, the callback is called on each and every HTTP redirect performed (at least, that was my experience when I wrote the C# equivalent to confirm this worked on Linux).
Any suggestions on how to leverage Add-Type yet only receiving the date information for the "target URL"?
Or any of your other alternatives, while respecting the previously mentioned goals?
The Add-Type cmdlet lets you define a Microsoft .NET Core class in your PowerShell session. You can then instantiate objects, by using the New-Object cmdlet, and use the objects just as you would use any .NET Core object. If you add an Add-Type command to your PowerShell profile, the class is available in all PowerShell sessions. You can specify the type by specifying an existing assembly or source code files, or you can specify the source code inline or saved in a variable. You can even specify only a method and Add-Type defines and generates the class. On Windows, you can use this feature to make Platform Invoke (P/Invoke) calls to unmanaged functions in PowerShell. If you specify source code, Add-Type compiles the specified source code and generates an in-memory assembly that contains the new .NET Core types. You can use the parameters of Add-Type to specify an alternate language and compiler, C# is the default, compiler options, assembly dependencies, the class namespace, the names of the type, and the resulting assembly. Beginning in PowerShell 7, Add-Type does not compile a type if a type with the same name already exists. Also, Add-Type looks for assemblies in a ref folder under the folder that contains pwsh.dll.
@iSazonov Why've you added the Resolution-Answered label already?
I sure don't feel as I've been given an answer that satisfies the conditions I made efforts to clarify up-front (see https://github.com/PowerShell/PowerShell/issues/13061#issuecomment-651756023).
Am I not allowed to edit issue as I uncover more specificity/details previously unknown to me?
Of course, if I was given a solution (which satisfied initial conditions), and decided I didn't like it, then by all means I could live with that.
I may have misunderstood in my ignorance, but as I understand it, that's _not_ the case as of yet.
You'd store your state in a static property. Or if it's not globally applicable you would make delegate source an instance method and create a new delegate/instance per invocation.
@iSazonov Why've you added the Resolution-Answered label already?
The bit that is relevant to the repo (e.g. "why doesn't this work") has already been answered. This issue board isn't really for support questions. @vexx32 and I don't mind helping you get where you're going though, chances are if we redirect you to the PowerShell discord it'll be one of us helping you there anyway 馃槈
Much appreciated for your follow-up answer and clarification @SeeminglyScience! =)
I'll continue trying to understand how delegate functions (or static PowerShell properties for that matter) work when work resumes again tomorrow.
If, as you say, that this issue has technically been answered, feel free to close it as such then!
I'll re-open new ones instead if I hit a new error I am unable to understand!
Why've you added the Resolution-Answered label already?
We can track nothing here for fixing or developing in PowerShell engine. Not every unexpected behavior is a bug. It is better to discuss such issues on other community resources like StackOverflow, forums and so on.
@x10an14 You'd basically want to do something like this:
Add-Type -TypeDefinition '
using System;
using System.Collections;
using System.Collections.Immutable;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public sealed class CCVCallbackClosure
{
public CCVCallbackClosure(Hashtable targetUrls)
{
TargetUrls = targetUrls.Keys
.Cast<object>()
.Select(k => LanguagePrimitives.ConvertTo<string>(k))
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
Callback = CallbackImpl;
}
public ImmutableHashSet<string> TargetUrls { get; }
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> Callback { get; }
private bool CallbackImpl(
HttpRequestMessage message,
X509Certificate2 cert,
X509Chain chain,
SslPolicyErrors policyErrors)
{
if (!TargetUrls.Contains(message.RequestUri.AbsoluteUri))
{
return true;
}
// Add other logic here.
return true;
}
}'
$targetUrls = @{
'https://outlook.com' = $null
'https://google.com' = $null
}
$closure = [CCVCallbackClosure]::new($targetUrls)
$handler.ServerCertificateCustomValidationCallback = $closure.Callback
# etc
Thanks a lot for the help all!
I got it working to my satisfaction now (although there's still room for improvement, such as proper async utilization).
Here it is for future reference if anyone else drops by with a similar issue;
Add-Type -AssemblyName System.Net.Http
$targetUrls = @{
"https://github.com" = $null
"https://google.com" = $null
"https://outlook.com" = $null
"https://microsoft.com" = $null
}
Add-Type -TypeDefinition '
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public sealed class CCVCallbackClosure {
public CCVCallbackClosure(string[] targetUrls) {
this.TargetUrls = targetUrls
.Select(targetUrl => new Uri(targetUrl))
.ToHashSet<Uri>();
this.Callback = CallbackImpl;
}
public HashSet<Uri> TargetUrls { get; }
public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> Callback { get; }
private bool CallbackImpl(
HttpRequestMessage message,
X509Certificate2 cert,
X509Chain chain,
SslPolicyErrors policyErrors
) {
if (TargetUrls.Contains(message.RequestUri)) {
var expirationTimeDelta = (
DateTime.Parse(cert.GetExpirationDateString()) -
DateTime.Today
).Days;
Console.WriteLine(
"\t" +
message.RequestUri + " expires in\t" +
expirationTimeDelta + " days!"
);
}
return SslPolicyErrors.None == policyErrors;
}
}'
$closure = [CCVCallbackClosure]::new($targetUrls.Keys)
# Set-up HttpClient & handler
$handler = New-Object -TypeName System.Net.Http.HttpClientHandler
$handler.ServerCertificateCustomValidationCallback = $closure.Callback
$client = New-Object -TypeName System.Net.Http.HttpClient -ArgumentList $handler
# Fetch HTTP Requests Async
$targetUrls.Keys.Clone() | ForEach-Object {
$targetUrls[$_] = $client.GetAsync($_).GetAwaiter()
}
# Await Async HTTP Requests to return
$targetUrls.GetEnumerator() | ForEach-Object {
# These two for-loops does NOT leverage the async functionality of the HTTP requests...
$responseMessage = $targetUrls[$_.Name].GetResult()
Write-Error "$($_.Name) System.Net.HttpStatusCode => $($responseMessage.StatusCode)."
}
Edit: Fix unintended bug where I didn't print the days left until expiration + separating between stdout and stderr.
Here's output of the above;
[2020-07-01 14:10:28] 0 x10an14@x10-desktop:~/Documents/bos/microsoft_certificates_test (master)
-> $ pwsh ./microsoft_powershell_certificates_test.ps1
https://google.com/ expires in 63 days!
https://microsoft.com/ expires in 697 days!
https://github.com/ expires in 678 days!
https://outlook.com/ expires in 140 days!
Write-Error: https://outlook.com System.Net.HttpStatusCode => OK.
Write-Error: https://microsoft.com System.Net.HttpStatusCode => OK.
Write-Error: https://google.com System.Net.HttpStatusCode => OK.
Write-Error: https://github.com System.Net.HttpStatusCode => OK.
[2020-07-01 14:10:44] 0 x10an14@x10-desktop:~/Documents/bos/microsoft_certificates_test (master)
-> $ pwsh ./microsoft_powershell_certificates_test.ps1 2>/dev/null
https://google.com/ expires in 63 days!
https://github.com/ expires in 678 days!
https://microsoft.com/ expires in 697 days!
https://outlook.com/ expires in 140 days!
[2020-07-01 14:10:58] 0 x10an14@x10-desktop:~/Documents/bos/microsoft_certificates_test (master)
-> $
Most helpful comment
When you assign by calling the function itself, you actually are _calling_ (invoking) the function by doing so; you're not assigning a delegate like C# does, you're just assigning the resultant value.
PowerShell _does_ generally have the capability (in some cases) to cast scriptblocks to
Func<,>types, for which you'll need to target the scriptblock itself:You also have the option of simply saving that scriptblock to a variable or directly assigning it rather than creating a named function if you prefer.
I haven't tested this, though, so perhaps you may find the built in conversions for this kind of thing insufficient to handle it.