Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
~~~
43 changes: 40 additions & 3 deletions README.md.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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

"@

Expand All @@ -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`.
Expand All @@ -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
~~~
"@
92 changes: 92 additions & 0 deletions Servers/MathServer.ps1
Original file line number Diff line number Diff line change
@@ -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 = @(
"<ul>"
foreach ($staticMember in $staticMembers) {
"<li>"
"<a href='/$($staticMember.name)'>$(
$staticMember.name
)</a>"
"</li>"
}
"</ul>"
) -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.
82 changes: 41 additions & 41 deletions Servers/Server101.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,46 @@
./Server101.ps1 ($pwd | Split-Path)
#>
param(
<# The Root Directory. #> [string]$RootDirectory = $PSScriptRoot,
# The Root Directory
[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 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"
})

$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
HttpListener = $httpListener ; ServerRoot = $RootDirectory
Files = [Ordered]@{}; ContentTypes = [Ordered]@{}
# Pack our job input into an IO dictionary
$io = [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] :
Expand All @@ -58,44 +57,45 @@ 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} else { $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
$fileStream.Close(), $fileStream.Dispose()
$reply.Close()
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
$method, $localPath =
$request.HttpMethod, $request.Url.LocalPath
$rq = $request = $getContext.Result.Request
$re = $reply = $getContext.Result.Response
$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,
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.
10 changes: 9 additions & 1 deletion Servers101.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
RootModule = 'Servers101.psm1'

# Version number of this module.
ModuleVersion = '0.1.1'
ModuleVersion = '0.1.2'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down Expand Up @@ -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:
Expand Down
Loading