PowerShell based web server
Summary
Recently, I needed to write a web server using PowerShell. PowerShell is a scripting language based on the dotnet framework. It can use dotnet libraries to build tools and different services. In this case, I needed to utilize the System.Net.HttpListener library. When I initially wrote this script, it needed to operate within a linux container. In many examples people post on the internet, the host IP or localhost IP is typically used. These options don’t work will when deployed with a container. The problem I encountered was specifically with the IP address. I’m not able to predict the IP address, so the best option is to use 0.0.0.0 or in my case +
. In this post, I’m only describing the code needed to create the PowerShell web server. The container configuration is out of scope.
Setting up a web server with PowerShell is very simple. In this example, the service will use 0.0.0.0 to listen for requests on port 8080, respond to the URI of /test, then return a GUID. The container I used was based on dotnet6 with PowerShell 7 running on a debian based container.
Setup
1
2
3
4
5
6
7
8
9
10
$urlPrefix = '+' # the plus is special, this tells it to listen on any IP.
# A specific IP can be used where the + is, but in a container the + is required.
$port = 8080 # port 80 is typically reserved, I found I had conflicts with dotnet in a container.
$path = 'test'
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add(("http://{0}:{1}/{2}" -f $UrlPrefix, $Port, ($path + '/') ))
$listener.Start() # starting the listener service.
# This will method will wait until a request is received.
$context = $listener.GetContext()
In the code, I’m using the URLPrefix with the magic +
in the place of the IP. The +
in dotnet is the equivalent of 0.0.0.0
which will be important for a container. Port 8080
was used instead of 80. I initially used 80 but at some point, I encountered a port conflict. I think this has something to do with how dotnet starts the listener (not really sure). To get around this I selected 8080
instead. There might be a special switch that can be disable the port 80 conflict but I didn’t find it.
The URL path is set to/test
. In this code, requests with /test will only receive a response. New-Object
was used to call the dotnet library and assign it to the variable $listener
. One thing to note with the URL, a closing slash /
is required by the HttpListener property. If an ending /
is not used, PowerShell will throw the following error.
1
MethodInvocationException: Exception calling "Add" with "1" argument(s): "Only Uri prefixes ending in '/' are allowed. (Parameter 'uriPrefix')"
Once, the prefix is assigned, Start() is called. As expected, this will start the port listener for the web server. Before the GetContext is executed, we can check the $listener
object and see it’s properties.
The $listener
object has a number of interest properties, IsListening, Prefixes, and AuthenticationSchemes. IsListening is boolean with a value set to True. Under Prefix it will show the assigned path. Once the GetContext() is executed we can test the server by connecting to localhost address, http://127.0.0.1:8080/test/.
System.Net.HttpListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$listener
AuthenticationSchemeSelectorDelegate :
ExtendedProtectionSelectorDelegate :
AuthenticationSchemes : Anonymous
ExtendedProtectionPolicy : ProtectionScenario=TransportSelected; PolicyEnforcement=Never;
CustomChannelBinding=<null>; ServiceNames=<null>
DefaultServiceNames : {HTTP/laptop}
Prefixes : {http://+:8080/test/}
Realm :
IsListening : True
IgnoreWriteExceptions : False
UnsafeConnectionNtlmAuthentication : False
TimeoutManager : System.Net.HttpListenerTimeoutManager
If we check the dotnet library, we can see the $listener
object has several properties and methods associated with it. To see an explanation of how to use the methods and properties visit the dotnet library here. Below is a list of all the methods and properties.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$listener | Get-Member
TypeName: System.Net.HttpListener
Name MemberType Definition
---- ---------- ----------
Abort Method void Abort()
BeginGetContext Method System.IAsyncResult BeginGetContext(System.AsyncCallback callback, Sys…
Close Method void Close()
Dispose Method void IDisposable.Dispose()
EndGetContext Method System.Net.HttpListenerContext EndGetContext(System.IAsyncResult async…
Equals Method bool Equals(System.Object obj)
GetContext Method System.Net.HttpListenerContext GetContext()
GetContextAsync Method System.Threading.Tasks.Task[System.Net.HttpListenerContext] GetContext…
GetHashCode Method int GetHashCode()
GetType Method type GetType()
Start Method void Start()
Stop Method void Stop()
ToString Method string ToString()
AuthenticationSchemes Property System.Net.AuthenticationSchemes AuthenticationSchemes {get;set;}
AuthenticationSchemeSelectorDelegate Property System.Net.AuthenticationSchemeSelector AuthenticationSchemeSelectorDe…
DefaultServiceNames Property System.Security.Authentication.ExtendedProtection.ServiceNameCollectio…
ExtendedProtectionPolicy Property System.Security.Authentication.ExtendedProtection.ExtendedProtectionPo…
ExtendedProtectionSelectorDelegate Property System.Net.HttpListener+ExtendedProtectionSelector ExtendedProtectionS…
IgnoreWriteExceptions Property bool IgnoreWriteExceptions {get;set;}
IsListening Property bool IsListening {get;}
Prefixes Property System.Net.HttpListenerPrefixCollection Prefixes {get;}
Realm Property string Realm {get;set;}
TimeoutManager Property System.Net.HttpListenerTimeoutManager TimeoutManager {get;}
UnsafeConnectionNtlmAuthentication Property bool UnsafeConnectionNtlmAuthentication {get;set;}
When a request is received by the server, GetContext will write to the assignment of $context
. Context has two Objects, Request and Response. $context.Request
will show a several attributes from System.Net.HttpListenerRequest class.
System.Net.HttpListenerContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$context.Request
AcceptTypes : {text/html, application/xhtml+xml, application/xml;q=0.9, image/webp…}
UserLanguages : {en-US, en;q=0.9, no;q=0.8}
Cookies : {}
ContentEncoding : System.Text.UTF8Encoding+UTF8EncodingSealed
ContentType :
IsLocal : True
IsWebSocketRequest : False
KeepAlive : True
QueryString : {}
RawUrl : /test/
UserAgent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63
UserHostAddress : 127.0.0.1:8080
UserHostName : 127.0.0.1:8080
UrlReferrer :
Url : http://127.0.0.1:8080/test/
ProtocolVersion : 1.1
ClientCertificateError :
RequestTraceIdentifier : 00000000-0000-0000-fd00-0040020000f8
ContentLength64 : 0
Headers : {sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, DNT…}
HttpMethod : GET
InputStream : System.IO.Stream+NullStream
IsAuthenticated : False
IsSecureConnection : False
ServiceName :
TransportContext : System.Net.HttpListenerRequestContext
HasEntityBody : False
RemoteEndPoint : 127.0.0.1:59296
LocalEndPoint : 127.0.0.1:8080
The $context.Request
object also has a number of useful settings. In this example RawUrl, and HttpMethod are used to identify the uri and request method that was used. From the output, it shows a GET
was sent to /test/
. My local port was sent from 59296
and the request went to 127.0.0.1:8080
.
Returning Data to the Client
1
2
3
4
5
6
7
8
9
if ($context.Request.HttpMethod -eq 'GET') {
if ($context.Request.RawUrl -match '/test$|/test/$') {
$context.Response.ContentType = 'application/html'
$content = [Text.Encoding]::UTF8.GetBytes((New-Guid).Guid)
}
}
$context.Response.OutputStream.Write($content, 0, $content.Length)
$context.Response.Close()
$listener.Stop()
Above, there are several if conditions that are checked. If it’s a GET
request and if it’s using the /test/
path continue. When the listener Prefixes is configured, the object required a closing slash /
. When users use a web service that ending /
may not be included.The if condition checks for both instances. If the request is a match, the content response is performed.
To respond to the request, a contentType is set and the content is assigned as to an array of bytes. $context.Response class has a property called OutputStream, this will utilize a Stream.Write Method write to the response object. The write stream needs the array of bytes, an offset and the length of the bytes. In this case, the offset is set to zero. After the response is set, the response is then closed and the listener can be stopped. The stop method will to shutdown the the web server service and release the port.
Microsoft.PowerShell.Commands.WebResponseObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Invoke-WebRequest -uri http://127.0.0.1:8080/test/ -OutVariable web
$web | fl *
Content : {53, 101, 50, 54…}
StatusCode : 200
StatusDescription : OK
RawContentStream : Microsoft.PowerShell.Commands.WebResponseContentMemoryStream
RawContentLength : 36
RawContent : HTTP/1.1 200 OK
Transfer-Encoding: chunked
Server: Microsoft-HTTPAPI/2.0
Date: Wed, 08 Mar 2023 05:24:37 GMT
Content-Type: application/html
5e2640dc-e695-41ea-823b-7be01090ed10
BaseResponse : StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content:
System.Net.Http.HttpConnectionResponseContent, Headers:
{
Transfer-Encoding: chunked
Server: Microsoft-HTTPAPI/2.0
Date: Wed, 08 Mar 2023 05:24:37 GMT
Content-Type: application/html
}
Headers : {[Transfer-Encoding, System.String[]], [Server, System.String[]], [Date, System.String[]],
[Content-Type, System.String[]]}
RelationLink : {}
From the client perspective, when the web server is listening for a Context request. A web request can be performed with Invoke-WebRequest or just opening a web browsers to http://127.0.0.1:8080/test/. The web server will receive the request and process it. The response in this case is a GUID string. The RawContent shows the header and of the response. The content shows the data as an array of bytes. To convert the bytes to a string, System.Text.Encoding is used resulting in a GUID.
1
2
[System.Text.Encoding]::ASCII.GetString($web.Content)
5e2640dc-e695-41ea-823b-7be01090ed10
I hope you found this useful.