The Series
Okay, so about 5 months ago I wrote a blog post on a potential method of executing PowerShell from a user interface built in HTML and javascript. It turns out that the series was one of the more popular ones I have posted and it consistently receives traffic.
The Problem
I did not really love my other method. It was just the only method I had found. The problems were:
- Complex to learn (build a server, build a client, learn HTTP methods)
- Possible security concerns (even though we were only listening on localhost a user could potentially gain access to another user session and execute code as a different user)
A MUCH Better method
So, this still is not cross-platform. I would still like to investigate ElectronJS and AppJS to see if integration with PowerShell would be easy to achieve and thus provide a method of making cross-platform apps using PowerShell, HTML and JavaScript but this will work for now on Windows devices and is pretty slick in my opinion.
Step 1 - Create a WPF web browser
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="$Title" Height="500" Width="700">
<Grid>
<DockPanel>
<WebBrowser Name="WebBrowser" DockPanel.Dock="Top" Margin="30">
</WebBrowser>
</DockPanel>
</Grid>
</Window>
"@
# Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Form=[Windows.Markup.XamlReader]::Load( $reader )
$WebBrowser = $Form.FindName("WebBrowser")
Step 2 - Create a C# class that is COMVisible for executing PowerShell
Okay, so this part was a little scary to figure out but it shouldn’t be too hard to figure out if you have been in the PowerShell space for awhile or tinkering with PowerShell v5 Classes. Basically we want something that will execute PowerShell code and that is ComVisible.
Add-Type -TypeDefinition @"
using System.Text;
using System.Runtime.InteropServices;
//Add For PowerShell Invocation
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
[ComVisible(true)]
public class PowerShellHelper
{
Runspace runspace;
public PowerShellHelper()
{
runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
}
public string InvokePowerShell(string cmd)
{
//Init stuff
RunspaceInvoke scriptInvoker = new RunspaceInvoke(runspace);
Pipeline pipeline;
pipeline = runspace.CreatePipeline();
//Add commands
pipeline.Commands.AddScript(cmd);
Collection<PSObject> results = pipeline.Invoke();
//Convert records to strings
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
stringBuilder.Append(obj);
}
return stringBuilder.ToString();
}
}
"@ -ReferencedAssemblies @("System.Management.Automation","Microsoft.CSharp")
Step 3 - (The magic part) Add a new PowerShellHelper Object as the ObjectForScripting on the WPF browser
$WebBrowser.ObjectForScripting = [PowerShellHelper]::new()
Step 4 - Run PowerShell code from HTML
$HTML = @'
<div id="results">Loading processes..</div>
<script>
function updateProcesses()
{
var HTMLProcessesFromPowerShell = window.external.InvokePowerShell("Get-Process | select name, id | convertTo-HTML");
document.getElementById("results").innerHTML = HTMLProcessesFromPowerShell;
}
updateProcesses();
</script>
'@
$WebBrowser.NavigateToString($HTML)
# ===========================================================================
# Shows the form
# ===========================================================================
write-host "To show the form, run the following" -ForegroundColor Cyan
$Form.ShowDialog() | out-null
The result:
Sweet! Now we have a simple method of executing PowerShell from HTML that does not require a server or require knowledge of HTTP. This code executes synchronously just like a good old HTA would have with your vbscript so the GUI will lock up while the code is running. If you want to play around with one that executes asynchronously and executes a javascript function with the results when the PowerShell script is finished running you can check out my gist here. The c# code is a bit more complicated there though. I may blog through this at a later date and possibly package it up as a module.
It might be nice to be able to wrap this up as something like Invoke-PSHTA -FilePath C:\MyHTMLApp.html right?
Let me know in the comments what you think of this method. I appreciate the read!