PowerShell als HTTPServer
PowerShell kann nicht nur als HTTPClient agieren und mit einem Webserver sprechen, sondern über den HTTPListener lässt sich mit PowerShell auch ganz schnell ein kleiner Webserver aufsetzen. Versuchen Sie nun aber nicht mit PowerShell einen IIS nachzubilden. Interessanter ist eher der Ansatz mit PowerShell einen kleinen REST-Server aufzubauen oder bestehende Skripte über einen HTTP-Nebeneingang abzufragen. Oder eben über den Weg über das Netzwerk Daten zu einem anderen PowerShell-Dienst zu senden.
Simpler HTTP Server
Die einfachste Form eines HTTP-Servers mit PowerShell besteht aus ganz wenigen Zeilen. Ein HTTP-Listener startet den Webserver, der ab dem Moment auf Anfragen wartet. Dann wartet das PowerShell-Skript auf einen Request um diesen dann zu verarbeiten. Der HTTPListener ist selbst schon als Thread gebaut, d.h. während das Skript einen Request verarbeitet, kann der nächste Request weiter eingehen und baut eine Queue auf.
Bei Windows 7/2008R2/2012 oder höher müssen Sie erst die Firewall informieren, dass eingehende Verbindungen erlaubt sind.
REM nicht erforderlich wenn man das Skript als Admin startet netsh http add URLacl URL=http://+:81/ User=domain\User
Hier ein Beispielcode, mit dem ich in dem Beispiel auf Port 81 lausche.
Write-host "Web Listener: Start" try { $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add('http://+:81/') # Must GENAU mit der Angabe in NETSH übereinstimmen $listener.Start() } catch { write-error "Unable to open listener. Check Admin permission or NETSH Binding" exit 1 } Write-host "Web Listener listening" $basename = (get-date -Format yyyyMMddHHmmss) $count = 0 $Host.UI.RawUI.FlushInputBuffer() [console]::TreatControlCAsInput = $true write-host "Press any key to end after the next incoming request" while (!([console]::KeyAvailable)) { $count++ write-host ("Listening on " + $listener.Prefixes ) $context = $listener.GetContext() # Warte auf eingehende Anfragen write-host "------- New Request ($count) arrived ------------" $request = $context.Request write-host (" URL.AbsoluteUri:" + $request.URL.AbsoluteUri) write-host (" HttpMethod :" + $request.HttpMethod) if ($request.HasEntityBody) { write-host "Exporting Body" # converting streamreader to string $rcvStream = [System.IO.StreamReader]::new($context.Request.InputStream).ReadToEnd() $rcvStream | out-file -filepath ("request"+$basename+$count+".txt") } else { write-host "No Body" } write-host "------- Sending OK Response ------------" $response = $context.Response $response.ContentType = 'text/plain' $message = "Anfrage verarbeitet" [byte[]] $buffer = [System.Text.Encoding]::UTF8.GetBytes($message) $response.ContentLength64 = $buffer.length $response.OutputStream.Write($buffer, 0, $buffer.length) $response.OutputStream.close() } $listener.Stop()
Diese einfache Version hat natürlich ein paar Einschränkungen:
- Unvollständige Exit-Routing
Es ist wichtig am Ende den Listener auch wieder zu stoppen. Wenn Sie das Skript mit "CTRL-C einfach abbrechen könnten, dann bleibt der Listener belegt und aktiv, bis die PowerShell geschlossen wird. Daher verhindert das Skript einen CTRL und beendet sich nach einem Tastendruck nach dem nächsten verarbeiteten Request. - Sicherheit (SSL/Auth)
Dieser ganz einfache Code enthält keine Funktion für eine Verschlüsselung per SSL oder Authentifizierung. Wer hier aktiv werden will, sollte vielleicht doch besser eine Applikation im IIS entwickeln. Das Rad muss man ja nicht mehrfach erfinden. - Serielle Abarbeitung
Dieses Beispielcode arbeitet einen Request nach dem anderen ab. Es ist eine reine serielle Abarbeitung und skaliert also nicht gut, wenn viele parallele Clients mit mehreren Threads Daten abrufen. Ein "lang dauernder Prozess" lähmt also andere nachfolgende Prozesse
Aber das Ziel ist ja nicht gleich einen Apache oder IIS-Wettbewerber zu entwickeln, sondern erst mal einen einfachen Service zu bauen, der per HTTP Daten annehmen und ausliefern kann. "Einfache" Beispiele gibt es hierzu genug.
- A Basic Powershell Webserver
https://gist.GitHub.com/jakobii/429dcef1bacacfa1da254a5353bbeac7 - MScholtes / WebServer
https://GitHub.com/MScholtes/WebServer - HTTPListener.psm1
https://www.powershellgallery.com/packages/HttpListener/1.0.2/Content/HTTPListener.psm1 - A HTTP file server in 130 lines of code
http://realfiction.net/2010/04/17/a-http-file-server-in-130-lines-of-code - PoshWebHarness.ps1
https://gist.GitHub.com/pmolchanov/0120a26a6ca8d88220a8 - Create a Web Server using PowerShell
https://hinchley.net/articles/create-a-web-server-using-powershell/ - Creating PowerShell Web Server
https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/creating-powershell-web-server - Und viele mehr
httplistener RequestObject und GetContext()
Solange Sie noch keinen Request mit "$listener.GetContext()" erwarten, lauscht der Listener zwar schon aber nach wenigen Sekunden kommt dann doch ein "Verbindung verweigert. Sobald sie aber einmal ein "GetContext()" gemacht haben, dann werden alle folgende Verbindungen angenommen und in einer Warteschlange abgelegt. Die Clients werden solange hingehalten. Dafür zuständig ist der "Timeout Manager. Hier die Defaults:
PS C:> $listener.TimeoutManager EntityBody : 00:00:00 DrainEntityBody : 00:00:00 RequestQueue : 00:00:00 IdleConnection : 00:00:00 HeaderWait : 00:00:00 MinSendBytesPerSecond : 0
- HttpListener.TimeoutManager Eigenschaft
https://docs.microsoft.com/de-de/dotnet/api/system.net.httplistener.timeoutmanager?view=net-5.0 - HttpListenerTimeoutManager Klasse
https://docs.microsoft.com/de-de/dotnet/api/system.net.httplistenertimeoutmanager?view=net-5.0
Das mit GetContext() erhaltene Objekte hat neben einem User nur zwei Eigenschaften, die ihrerseits wieder Objekte vom Typ "System.Net.HttpListenerRequest" und "System.Net.HttpListenerResponse" sind:
- HttpListenerRequest Klasse
https://docs.microsoft.com/de-de/dotnet/api/system.net.httplistenerrequest?view=net-5.0 - HttpListenerResponse Klasse
https://docs.microsoft.com/de-de/dotnet/api/system.net.httplistenerresponse?view=net-5.0
Hier ein Beispiel-Request
PS C:\> $context = $listener.GetContext() PS C:\> $context | fl * Request : System.Net.HttpListenerRequest User : Response : System.Net.HttpListenerResponse PS C:\> $context.Request AcceptTypes : UserLanguages : Cookies : {} ContentEncoding : System.Text.UTF8Encoding+UTF8EncodingSealed ContentType : text/xml; charset=utf-8 IsLocal : True IsWebSocketRequest : False KeepAlive : True QueryString : {} RawUrl : /autodiscover/autodiscover.xml UserAgent : test-autodweiche/1.0 UserHostAddress : 192.168.178.91:80 UserHostName : 192.168.178.91 UrlReferrer : Url : http://192.168.178.91/autodiscover/autodiscover.xml ProtocolVersion : 1.1 ClientCertificateError : RequestTraceIdentifier : 00000000-0000-0000-f500-0080000000fe ContentLength64 : 354 Headers : {Content-Length, Content-Type, Host, User-Agent} HttpMethod : POST InputStream : System.Net.HttpRequestStream IsAuthenticated : False IsSecureConnection : False ServiceName : TransportContext : System.Net.HttpListenerRequestContext HasEntityBody : True RemoteEndPoint : 192.168.178.91:8139 LocalEndPoint : 192.168.178.91:80
Der "Inputstream" kann übrigens nur einmal gelesen werden. Sie sollten die Ergebnisse daher speicher, wenn Sie diese mehrfach parsen wollen.
Das "Context-Objekt" hat auch eine "Response"-Eigenschaft, die Sie mit Werten füllen sollten, ehe Sie die Verarbeitung mit folgender Zeile beenden.
$context.Response.OutputStream.Close()
Die Variable ist danach immer noch instanziert aber sie können natürlich nicht mehr viel damit anfangen, da kein Client drauf wartet.
Leider gibt meines Wissen leider keinen Weg den Listener zu fragen, wie viele ausstehende Requests derzeit in der Warteschlange sind noch den Listener zu starten und ohne Ergebnis früher zurückkommen zu lassen.
HTTPListener mit BeginGetContext
Den einfachen Beispielen ist alles gemeinsam, dass Sie mit "$listener.GetContext()" endlos auf einen Request warten und dann diesen erst Abarbeiten, ehe Sie den nächsten Request bedienen. Wenn Sie Skript einfach abbrechen, bleibt der Listener weiter aktiv und blockiert den weiteren Start, bis Sie die Powershell-Session schließen und neu starten. Zudem werden alle Requests streng "Sequentiell" abgearbeitet. Das ist natürlich beides nicht optimal aber der Einfachheit geschuldet. Es geht aber auch anders, denn über die Methode "BeginGetContext()" ist eine asynchrone Verarbeitung möglich. Sie müssen dann aber eine "CallBack"-Funktion hinterlegen, die beim Eintreffen eines Requests ausgeführt wird.
Damit steigt der Anspruch an den Code, z:B. wie die konkurrierende Zugriffe auf gleiche Inhalte synchronisieren und sich gegen "Überlastungen" wehren. Zudem sollten Sie den Unterschied zwischen "Multithreading" und "Asynchroner Verarbeitung mittels Callbacks verstehen.
Per Callback kann ich Code hinterlegen, der von einer Funktion aufgerufen wird. Allerdings ist das keine parallele Verarbeitung in einem eigenen Prozess oder Thread. PowerShell wartet einfach den aktuellen Befehl ab, um dann die Callback-Routine durchzuführen. Diese hat aber ihre eigene Laufzeitumgebung, d.h. hat keinen Durchgriff auf ihre Variablen, Sie müssen schon irgendwie eine "Übergabe" organisieren. Alles nicht ganz einfach aber ich habe auch hierfür ein "Sample" bereitgestellt:
powershell/sample-webserverasync.ps1.txt
Einfach in einer PowerShell als Admin aufrufen und per
Browser auf http://localhost:8080 gehen. Mit
http://localhost:8080/date sehen Sie das Computerdatum und
mit http://localhost:8080/quit beenden Sie das Skript. Ein
Tastendruck tut es aber auch. Damit sehen Sie, dass die
"Hauptschleife" durchaus etwas tun kann
Schön ist es dennoch nicht, denn das Hauptprogramm läuft auch hier in einer Endlosschleife. Besser wäre ein "Idle"-Kommando, um auf den Callback oder einen Timeout zu warten.
Vermeiden Sie aber unbedingt ein "Start-Sleep" einzusetzen, denn der CallBack wird immer erst nach dem Ende das aktuell laufenden Kommandos ausgeführt. Es ist kein Multitasking oder Multithreading
- PowerShell und Callback-Funktionen
- HttpListener.BeginGetContext(AsyncCallback, Object) Methode
https://docs.microsoft.com/de-de/dotnet/api/system.net.httplistener.begingetcontext?view=net-5.0 - Start-HealthZListener.ps1
https://www.powershellgallery.com/packages/PSHealthZ/1.0.0/Content/Public%5CStart-HealthZListener.ps1 - PowerShell Web Service/Rest API with Asynchronous request handling
https://gist.GitHub.com/Diagg/f57b330f340e4f42fed26dd5759cca05 - Using Background Runspaces Instead of
PSJobs For Better Performance
https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/
Grenzen von HTTPListener
Die HTTPListener-Klasse ist eine schöne einfache Option per PowerShell eine Schnittstelle anzubieten, mit der ich Aktionen übergeben kann. Sie kommt aber ohne aufwändige Programmierung nicht an einen "richtigen" Webserver heran. Für den Notfall könnten Sie sogar per ASP-Seite einfach ein Skript aufrufen.
Die Eingabe werden erst in einem Webformular abgefragt, welches dann eine ASP-Seite direkt ausführt:
<form action="RunPowershell.asp" method="post"> <input type="textbox" name="Number"/> <input type="submit"/> </form>
Die dann die RunPowershell.asp mit den Parametern aufruft.
<% Dim phoneNumber phoneNumber = Request.Form("phoneNumber") Server.Execute("c:\psasp\powershell.ps1 -Rufnummer " & Number) %>
Hier wird dann jedes Mal ein PowerShell-Prozess im Context des Webservers gestartet.
Das ist nicht schnell aber vor allem unsicher.
Da ist mein Ansatz von ReportWeb noch besser, bei dem eine ASP-Seite einen "Job" in Auftrag gibt, der dann von einem Worker mit anderen Rechten ausgeführt wird und sich Gedanken über Throttling u.a. macht. Wobei das dann alles nicht mehr weit von der alten "CGI-Schnittstelle" entfernt ist.
- Common Gateway Interface
https://en.wikipedia.org/wiki/Common_Gateway_Interface
Eine interessante kommerzielle Option ist PowerShell.ASP
- PowerShell ASP: Create Dyanmic Web
Content With PowerShell
https://www.nsoftware.com/kb/articles/psasp-create-dynamic-web-content.rst - Mittlerweile Bestandteil von "PowerShell
Server"
https://www.nsoftware.com/powershell/server/
https://www.nsoftware.com/kb/articles/psserver-getting-started.rst#webserver - PowerShell ASP – Too cool!
https://devblogs.microsoft.com/powershell/powershell-asp-too-cool/
Eine andere Option wäre die Nutzung anderer Frameworks, um Powershell oder andere Skripte per WebServer einfach zu starten
- PowerShell / Polaris
https://GitHub.com/PowerShell/Polaris - How to Create a PowerShell Powered Web Server Using Pode
https://badgerati.GitHub.io/Pode/
https://petri.com/how-to-create-a-powershell-powered-web-server-using-pode - Universal Dashboard (nun PowerShell Universal)
https://docs.powershelluniversal.com/
https://docs.universaldashboard.io/webserver - Kestrel web server implementation in
ASP.NET Core
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-2.1
Polaris und PSHTML
Natürlich können Sie nun einen Webserver selbst in Powershell schreiben. Aber auch hier gibt es schon fertige Lösungen, die sich um die meisten Dinge alleine kümmern. Exemplarisch können Sie dazu Polaris und PSHTML anschauen.
- Polaris
https://GitHub.com/PowerShell/Polaris
Ein Modul ,welches einen Webserver startet und die Rückgaben über eigene PowerShell-Code erzeugt. - PSHTML
Ein Framework um elegant HTML-Ausgaben zu erzeugen
https://pshtml.readthedocs.io/en/latest
https://GitHub.com/Stephanevg/PSHTML
http://chen.about-powershell.com/2018/12/experiment-using-pshtml-in-node-js-and-ost-a-azure-web-app/
http://chen.about-powershell.com/2019/01/build-web-app-using-powershell-pshtml-module-in-go-lang
Stéphane van Gulick - Creating and
hosting beautiful websites with PSHTML & Polaris
https://www.youtube.com/watch?v=X6ZtS7rWQ9M
Es gibt noch einige andere dieser "Mini Frameworks"
- https://GitHub.com/straightdave/presley
- https://GitHub.com/StartAutomating/Pipeworks
- https://GitHub.com/toenuff/flancy
- https://GitHub.com/Jaykul/NancyPS/
- https://GitHub.com/toenuff/PshOdata
- https://GitHub.com/cofonseca/WebListener
- https://GitHub.com/DataBooster/PS-WebApi
- https://GitHub.com/ChristopherGLewis/PowerShellWebServers
Weitere Links
- PowerShell
- ErrHandling
- PowerShell-Skript als Dienst
-
RESTful Server
http://poshcode.org/4073 -
PowerShell Web Server
http://www.poshserver.net/index.html
WebServer in PowerShell geschrieben -
PowerShell Server
http://www.PowerShellserver.com/overview/web/
PowerShell Code als PS1X (analog zu ASPX) einbetten und dynamische Webseiten erzeugen lassen. -
Simple HTTP Api for Executing
PowerShell Scripts
https://devblogs.microsoft.com/powershell/simple-http-api-for-executing-powershell-scripts/ -
Smple PowerShell HTTP/HTTPS web
server useful for mocking web
services
https://gist.GitHub.com/pmolchanov/0120a26a6ca8d88220a8