From 2b6a08837ce58b4792620c316dc15efb98b94e7a Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 19:44:44 -0800 Subject: [PATCH 1/8] feat: Server101 Error Support ( Fixes #18 ) --- Servers/Server101.ps1 | 66 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Servers/Server101.ps1 b/Servers/Server101.ps1 index 030d95b..335f977 100644 --- a/Servers/Server101.ps1 +++ b/Servers/Server101.ps1 @@ -7,18 +7,17 @@ ./Server101.ps1 ($pwd | Split-Path) #> param( -<# The Root Directory. #> [string]$RootDirectory = $PSScriptRoot, +# The Root Directory +[string]$RootPath = $(if ($PSScriptRoot) {$PSScriptRoot } else { $pwd }), # The rootUrl of the server. By default, a random loopback address. -[string]$RootUrl= - "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/", +[string]$RootUrl="http://127.0.0.1:$(Get-Random -Minimum 4kb -Maximum 42kb)/", # The type map. This determines how each extension will be served. -[Collections.IDictionary] -$TypeMap = [Ordered]@{ - ".html" = "text/html" ; ".css" = "text/css" ; ".svg" = "image/svg+xml" ; - ".png" = "image/png" ; ".jpg" = "image/jpeg"; ".gif" = "image/gif" - ".mp3" = "audio/mpeg"; ".mp4" = "video/mp4" +[Collections.IDictionary]$TypeMap = [Ordered]@{ + ".html" = "text/html" ; ".css" = "text/css" ; ".svg" = "image/svg+xml" + ".png" = "image/png" ; ".jpg" = "image/jpeg" ; ".gif" = "image/gif" + ".oog" = "audio/oog" ; ".mp3" = "audio/mpeg"; ".mp4" = "video/mp4" ".json" = "application/json"; ".xml" = "application/xml" ; ".js" = "text/javascript" ; ".jsm" = "text/javascript" ; ".ps1" = "text/x-powershell" @@ -29,25 +28,25 @@ $httpListener.Prefixes.Add($RootUrl) Write-Warning "Listening on $RootUrl $($httpListener.Start())" $io = [Ordered]@{ # Pack our job input into an IO dictionary - HttpListener = $httpListener ; ServerRoot = $RootDirectory - Files = [Ordered]@{}; ContentTypes = [Ordered]@{} + HttpListener = $httpListener ; ServerRoot = $RootPath + Files = [Ordered]@{}; ContentTypes = [Ordered]@{} } # Then map each file into one or more /uris -foreach ($file in Get-ChildItem -File -Path $RootDirectory -Recurse) { +foreach ($file in Get-ChildItem -File -Path $RootPath -Recurse) { $relativePath = - $file.FullName.Substring($RootDirectory.Length) -replace '[\\/]', '/' + $file.FullName.Substring($RootPath.Length) -replace '[\\/]', '/' $fileUris = @($relativePath) + @( - foreach ($indexFile in 'index.html', 'readme.html') { - $indexPattern = [Regex]::Escape($indexFile) + '$' + foreach ($indexFile in 'index.html', 'readme.html') { + $indexPattern = [Regex]::Escape($indexFile) + '$' if ($file.Name -eq $indexFile -and -not $IO.Files[ $relativePath -replace $indexPattern - ]) { + ]) { $relativePath -replace $indexPattern $relativePath -replace "[\\/]$indexPattern" } } ) - foreach ($fileUri in $fileUris) { + foreach ($fileUri in $fileUris) { $io.ContentTypes[$fileUri] = # and map content types now $TypeMap[$file.Extension] ? # so we don't have to later. $TypeMap[$file.Extension] : @@ -58,30 +57,32 @@ foreach ($file in Get-ChildItem -File -Path $RootDirectory -Recurse) { # Our server is a thread job Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) - $psvariable = $ExecutionContext.SessionState.PSVariable - foreach ($key in $io.Keys) { # First, let's unpack - if ($io[$key] -is [PSVariable]) { $psvariable.set($io[$key]) } - else { $psvariable.set($key, $io[$key]) } - } # and then declare a few filters to make code more readable. - filter outputError([int]$Number) { - $reply.StatusCode = $Number; $reply.Close(); continue nextRequest + $psvar = $ExecutionContext.SessionState.PSVariable + foreach ($k in $io.Keys) { $psvar.set($k, $io[$k]) } + filter outputError([int]$N) { + $re.StatusCode = $N + $localPath = "/$N.html";$file = $files[$LocalPath] + if ($file) {outputFile} + $re.Close() + continue next } filter outputHeader { - $reply.Length=$file.Length; $reply.Close(); continue nextRequest + $re.Length=$files[$localPath].Length;$re.Close() + continue next } filter outputFile { $reply.ContentType = $contentTypes[$localPath] $fileStream = $file.OpenRead() $fileStream.CopyTo($reply.OutputStream) $fileStream.Close(); $fileStream.Dispose(); $reply.Close() - continue nextRequest + continue next } - # Listen for the next request - :nextRequest while ($httpListener.IsListening) { + # Listen for the next request and reply to it. + :next while ($httpListener.IsListening) { $getContext = $httpListener.GetContextAsync() while (-not $getContext.Wait(17)) { } - $request, $reply = # and reply to it. - $getContext.Result.Request, $getContext.Result.Response + $rq = $request = $getContext.Result.Request + $re = $reply = $getContext.Result.Response $method, $localPath = $request.HttpMethod, $request.Url.LocalPath # If the method is not allowed, output error 405 @@ -94,8 +95,7 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) outputFile # otherwise, output the file. } } -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job, - Add-Member -NotePropertyMembers @{ # but attach a few properties first: - HttpListener=$httpListener # * The listener (so we can stop it) - IO=$IO # * The IO (so we can change it) - Url="$RootUrl" # The URL (so we can easily access it). + Add-Member -NotePropertyMembers @{ # and attach a few properties: + # `.HttpListener`, `.IO`, `.URL` + HttpListener=$httpListener; IO=$IO; Url="$RootUrl" } -Force -PassThru # Pass all of that thru and return it to you. \ No newline at end of file From e03e89b3da747736957548b763e043ba062f9860 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 19:49:41 -0800 Subject: [PATCH 2/8] feat: MathServer ( Fixes #14 ) --- Servers/MathServer.ps1 | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Servers/MathServer.ps1 diff --git a/Servers/MathServer.ps1 b/Servers/MathServer.ps1 new file mode 100644 index 0000000..8bf3805 --- /dev/null +++ b/Servers/MathServer.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + Math Server +.DESCRIPTION + A simple math server. + + Each first request segment must map to a static method on the `[Math]` class +.EXAMPLE + ./MathServer.ps1 +#> +param( +# The rootUrl of the server. By default, a random loopback address. +[string]$RootUrl= + "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/" +) + +$httpListener = [Net.HttpListener]::new() +$httpListener.Prefixes.Add($RootUrl) +Write-Warning "Listening on $RootUrl $($httpListener.Start())" + +$io = [Ordered]@{ # Pack our job input into an IO dictionary + HttpListener = $httpListener +} + +# Our server is a thread job +Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) + $psvariable = $ExecutionContext.SessionState.PSVariable + foreach ($key in $io.Keys) { # First, let's unpack. + if ($io[$key] -is [PSVariable]) { $psvariable.set($io[$key]) } + else { $psvariable.set($key, $io[$key]) } + } + $staticMembers = [Math] | Get-Member -Static + # Listen for the next request + :nextRequest while ($httpListener.IsListening) { + $getContext = $httpListener.GetContextAsync() + while (-not $getContext.Wait(17)) { } + $request, $reply = + $getContext.Result.Request, $getContext.Result.Response + + $segments = @($request.Url.Segments) + if ($segments.Length -le 1) { + $reply.ContentType = 'text/html' + $memberList = @( + "" + ) -join [Environment]::NewLine + $reply.Close($OutputEncoding.GetBytes($memberList), $false) + continue nextRequest + } else { + $firstSegment = $segments[1] -replace '/' + $mathMember = [Math]::$firstSegment + if ($null -eq $mathMember) { + $reply.StatusCode = 404 + $reply.Close() + continue nextRequest + } + } + + if ($mathMember.Invoke) { + $mathArgs = + if ($segments.Length -ge 3) { + @(for ($segmentNumber = 2; $segmentNumber -lt $segments.Length; $segmentNumber++) { + $segments[$segmentNumber] -replace '/' -as [double] + }) + } else { + $Reply.Close($OutputEncoding.GetBytes("$( + $mathMember.OverloadDefinitions -join [Environment]::NewLine + )"), $false) + } + try { + $result = $mathMember.Invoke($mathArgs) + $Reply.Close($OutputEncoding.GetBytes("$result"), $false) + } catch { + $Reply.Close($OutputEncoding.GetBytes("$($_ | Out-String)"), $false) + } + } else { + $Reply.Close($OutputEncoding.GetBytes("$mathMember"), $false) + } + } +} -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job, + Add-Member -NotePropertyMembers @{ # but attach a few properties first: + HttpListener=$httpListener # * The listener (so we can stop it) + IO=$IO # * The IO (so we can change it) + Url="$RootUrl" # The URL (so we can easily access it). + } -Force -PassThru # Pass all of that thru and return it to you. \ No newline at end of file From 398186b5fa8099ca3ab2e20e9bdacbb69603c90f Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:11:47 -0800 Subject: [PATCH 3/8] feat: Server101 Error Support ( Fixes #18 ) Minor touch-ups --- Servers/Server101.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Servers/Server101.ps1 b/Servers/Server101.ps1 index 335f977..6c21e70 100644 --- a/Servers/Server101.ps1 +++ b/Servers/Server101.ps1 @@ -67,7 +67,8 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) continue next } filter outputHeader { - $re.Length=$files[$localPath].Length;$re.Close() + $re.Length=$files[$localPath].Length + $re.Close() continue next } filter outputFile { @@ -83,8 +84,7 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) while (-not $getContext.Wait(17)) { } $rq = $request = $getContext.Result.Request $re = $reply = $getContext.Result.Response - $method, $localPath = - $request.HttpMethod, $request.Url.LocalPath + $method, $localPath = $re.HttpMethod, $re.Url.LocalPath # If the method is not allowed, output error 405 if ($method -notin 'get', 'head') { outputError 405 } # If the file does not exist, output error 404 From 5ad948420e612b0464b80d8dc0acbb5a0dc8f8d5 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:27:30 -0800 Subject: [PATCH 4/8] feat: Server101 Error Support ( Fixes #18 ) Minor touch-ups --- Servers/Server101.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Servers/Server101.ps1 b/Servers/Server101.ps1 index 6c21e70..2aedda3 100644 --- a/Servers/Server101.ps1 +++ b/Servers/Server101.ps1 @@ -84,14 +84,14 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) while (-not $getContext.Wait(17)) { } $rq = $request = $getContext.Result.Request $re = $reply = $getContext.Result.Response - $method, $localPath = $re.HttpMethod, $re.Url.LocalPath + $method, $localPath = $rq.HttpMethod, $rq.Url.LocalPath # If the method is not allowed, output error 405 if ($method -notin 'get', 'head') { outputError 405 } # If the file does not exist, output error 404 if (-not ($files -and $files[$localPath])) { outputError 404 } $file = $files[$localPath] # If they asked for header information, output it. - if ($request.httpMethod -eq 'head') { outputHeader } + if ($method -eq 'head') { outputHeader } outputFile # otherwise, output the file. } } -ThrottleLimit 100 -ArgumentList $IO -Name "$RootUrl" | # Output our job, From 38514b6a578da5c16d3bb1b5211b7c456f0adfd0 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:32:11 -0800 Subject: [PATCH 5/8] feat: Server101 Error Support ( Fixes #18 ) Aliasing parameters --- Servers/Server101.ps1 | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Servers/Server101.ps1 b/Servers/Server101.ps1 index 2aedda3..bf5029c 100644 --- a/Servers/Server101.ps1 +++ b/Servers/Server101.ps1 @@ -8,7 +8,8 @@ #> param( # The Root Directory -[string]$RootPath = $(if ($PSScriptRoot) {$PSScriptRoot } else { $pwd }), +[Alias('RootDirectory')][string]$RootPath = + $(if ($PSScriptRoot) {$PSScriptRoot } else { $pwd }), # The rootUrl of the server. By default, a random loopback address. [string]$RootUrl="http://127.0.0.1:$(Get-Random -Minimum 4kb -Maximum 42kb)/", @@ -23,11 +24,10 @@ param( ".ps1" = "text/x-powershell" }) -$httpListener = [Net.HttpListener]::new() -$httpListener.Prefixes.Add($RootUrl) +$httpListener = [Net.HttpListener]::new();$httpListener.Prefixes.Add($RootUrl) Write-Warning "Listening on $RootUrl $($httpListener.Start())" - -$io = [Ordered]@{ # Pack our job input into an IO dictionary +# Pack our job input into an IO dictionary +$io = [Ordered]@{ HttpListener = $httpListener ; ServerRoot = $RootPath Files = [Ordered]@{}; ContentTypes = [Ordered]@{} } @@ -62,14 +62,12 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) filter outputError([int]$N) { $re.StatusCode = $N $localPath = "/$N.html";$file = $files[$LocalPath] - if ($file) {outputFile} - $re.Close() + if ($file) {outputFile} else { $re.Close() } continue next } filter outputHeader { $re.Length=$files[$localPath].Length - $re.Close() - continue next + $re.Close();continue next } filter outputFile { $reply.ContentType = $contentTypes[$localPath] From 5e21495be469ee0e6a4a0e82825fb3c314059116 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:32:50 -0800 Subject: [PATCH 6/8] feat: Server101 Error Support ( Fixes #18 ) Spreading out --- Servers/Server101.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Servers/Server101.ps1 b/Servers/Server101.ps1 index bf5029c..bdd9df5 100644 --- a/Servers/Server101.ps1 +++ b/Servers/Server101.ps1 @@ -67,13 +67,15 @@ Start-ThreadJob -ScriptBlock {param([Collections.IDictionary]$io) } filter outputHeader { $re.Length=$files[$localPath].Length - $re.Close();continue next + $re.Close() + continue next } filter outputFile { $reply.ContentType = $contentTypes[$localPath] $fileStream = $file.OpenRead() $fileStream.CopyTo($reply.OutputStream) - $fileStream.Close(); $fileStream.Dispose(); $reply.Close() + $fileStream.Close(), $fileStream.Dispose() + $reply.Close() continue next } # Listen for the next request and reply to it. From 7bbddaa44a0ba053e0adbea729922c87f955e7b2 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:37:22 -0800 Subject: [PATCH 7/8] docs: README updates ( Fixes #19 ) --- README.md | 39 +++++++++++++++++++++++++++++++++++++-- README.md.ps1 | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a420747..ee83684 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,30 @@ This is a collection of simple servers in PowerShell. Feel free to [contribute](contributing.md) and add your own. - -## Server Samples +## Sample Servers * [DebugServer.ps1](/Servers/DebugServer.ps1) * [DualEventServer.ps1](/Servers/DualEventServer.ps1) * [EventServer.ps1](/Servers/EventServer.ps1) +* [MathServer.ps1](/Servers/MathServer.ps1) * [MethodSwitchServer.ps1](/Servers/MethodSwitchServer.ps1) * [Server101.ps1](/Servers/Server101.ps1) * [SwitchRegexServer.ps1](/Servers/SwitchRegexServer.ps1) +## Installing + +You can install Servers101 from the [PowerShell Gallery](https://powershellgallery.com) + +~~~PowerShell +Install-Module Servers101 +~~~ + +Once installed, you can import it: + +~~~PowerShell +Import-Module Servers101 -PassThru +~~~ + ## Using this module This module has only one command, Get-Servers101. @@ -39,3 +53,24 @@ Each server will be self-contained in a single script. To start the server, simply run the script. To learn about how each server works, read thru each script. + +## Streaming Server101 + +Because each server is contained within a single file, the servers can be streamed to a file + +For example, to start a local file server, we can run: + +~~~PowerShell +Invoke-RestMethod https://cdn.jsdelivr.net/gh/PoshWeb/Servers101@latest/Servers/Server101.ps1 > ./server.ps1; +./server.ps1 +~~~ + +For some servers it is also possible to run with Invoke-Expression. + +You should never Invoke-Expression code you cannot trust and verify. + +To stream a local file server, we can run: + +~~~PowerShell +irm https://cdn.jsdelivr.net/gh/PoshWeb/Servers101@latest/Servers/Server101.ps1 | iex +~~~ diff --git a/README.md.ps1 b/README.md.ps1 index b537fa4..2b4d432 100644 --- a/README.md.ps1 +++ b/README.md.ps1 @@ -19,8 +19,7 @@ This is a collection of simple servers in PowerShell. Feel free to [contribute](contributing.md) and add your own. - -## Server Samples +## Sample Servers "@ @@ -30,9 +29,22 @@ foreach ($serverScript in Get-Servers101) { } - @" +## Installing + +You can install Servers101 from the [PowerShell Gallery](https://powershellgallery.com) + +~~~PowerShell +Install-Module Servers101 +~~~ + +Once installed, you can import it: + +~~~PowerShell +Import-Module Servers101 -PassThru +~~~ + ## Using this module This module has only one command, `Get-Servers101`. @@ -44,4 +56,29 @@ Each server will be self-contained in a single script. To start the server, simply run the script. To learn about how each server works, read thru each script. +"@ + + +@" + +## Streaming Server101 + +Because each server is contained within a single file, the servers can be streamed to a file + +For example, to start a local file server, we can run: + +~~~PowerShell +Invoke-RestMethod https://cdn.jsdelivr.net/gh/PoshWeb/Servers101@latest/Servers/Server101.ps1 > ./server.ps1; +./server.ps1 +~~~ + +For some servers it is also possible to run with Invoke-Expression. + +You should never Invoke-Expression code you cannot trust and verify. + +To stream a local file server, we can run: + +~~~PowerShell +irm https://cdn.jsdelivr.net/gh/PoshWeb/Servers101@latest/Servers/Server101.ps1 | iex +~~~ "@ \ No newline at end of file From f8b0bb2f1f10683b61cc106fc5c012c37ba2d417 Mon Sep 17 00:00:00 2001 From: StartAutomating Date: Mon, 23 Feb 2026 21:41:29 -0800 Subject: [PATCH 8/8] release: Servers101 0.1.2 Updating CHANGELOG and Release Notes --- CHANGELOG.md | 9 +++++++++ Servers101.psd1 | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13be87a..3e28a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Servers101 0.1.2: + +* Added Error Code Support to Server101 (#18) +* MathServer (#15) +* README installation and streaming server instructions (#19) + +--- + + ## Servers101 0.1.1: * New Servers: diff --git a/Servers101.psd1 b/Servers101.psd1 index ee12d79..b15d398 100644 --- a/Servers101.psd1 +++ b/Servers101.psd1 @@ -12,7 +12,7 @@ RootModule = 'Servers101.psm1' # Version number of this module. -ModuleVersion = '0.1.1' +ModuleVersion = '0.1.2' # Supported PSEditions # CompatiblePSEditions = @() @@ -108,6 +108,14 @@ PrivateData = @{ # ReleaseNotes of this module ReleaseNotes = @' +## Servers101 0.1.2: + +* Added Error Code Support to Server101 (#18) +* MathServer (#15) +* README installation and streaming server instructions (#19) + +--- + ## Servers101 0.1.1: * New Servers: