1+ <#
2+ . SYNOPSIS
3+ Server 101
4+ . DESCRIPTION
5+ Server 101: A file server in 101 lines of pure PowerShell.
6+ . EXAMPLE
7+ ./Server101.ps1 ($pwd | Split-Path)
8+ #>
9+ param (
10+ <# The Root Directory. #> [string ]$RootDirectory = $PSScriptRoot ,
11+
12+ # The rootUrl of the server. By default, a random loopback address.
13+ [string ]$RootUrl =
14+ " http://127.0.0.1:$ ( Get-Random - Minimum 4200 - Maximum 42000 ) /" ,
15+
16+ # The type map. This determines how each extension will be served.
17+ [Collections.IDictionary ]
18+ $TypeMap = [Ordered ]@ {
19+ " .html" = " text/html" ; " .css" = " text/css" ; " .svg" = " image/svg+xml" ;
20+ " .png" = " image/png" ; " .jpg" = " image/jpeg" ; " .gif" = " image/gif"
21+ " .mp3" = " audio/mpeg" ; " .mp4" = " video/mp4"
22+ " .json" = " application/json" ; " .xml" = " application/xml" ;
23+ " .js" = " text/javascript" ; " .jsm" = " text/javascript" ;
24+ " .ps1" = " text/x-powershell"
25+ })
26+
27+ $httpListener = [Net.HttpListener ]::new()
28+ $httpListener.Prefixes.Add ($RootUrl )
29+ Write-Warning " Listening on $RootUrl $ ( $httpListener.Start ()) "
30+
31+ $io = [Ordered ]@ { # Pack our job input into an IO dictionary
32+ HttpListener = $httpListener ; ServerRoot = $RootDirectory
33+ Files = [Ordered ]@ {}; ContentTypes = [Ordered ]@ {}
34+ }
35+ # Then map each file into one or more /uris
36+ foreach ($file in Get-ChildItem - File - Path $RootDirectory - Recurse) {
37+ $relativePath =
38+ $file.FullName.Substring ($RootDirectory.Length ) -replace ' [\\/]' , ' /'
39+ $fileUris = @ ($relativePath ) + @ (
40+ foreach ($indexFile in ' index.html' , ' readme.html' ) {
41+ $indexPattern = [Regex ]::Escape($indexFile ) + ' $'
42+ if ($file.Name -eq $indexFile -and -not $IO.Files [
43+ $relativePath -replace $indexPattern
44+ ]) {
45+ $relativePath -replace $indexPattern
46+ $relativePath -replace " [\\/]$indexPattern "
47+ }
48+ }
49+ )
50+ foreach ($fileUri in $fileUris ) {
51+ $io.ContentTypes [$fileUri ] = # and map content types now
52+ $TypeMap [$file.Extension ] ? # so we don't have to later.
53+ $TypeMap [$file.Extension ] :
54+ ' text/plain'
55+ $io.Files [$fileUri ] = $file
56+ }
57+ }
58+
59+ # Our server is a thread job
60+ Start-ThreadJob - ScriptBlock {param ([Collections.IDictionary ]$io )
61+ $psvariable = $ExecutionContext.SessionState.PSVariable
62+ foreach ($key in $io.Keys ) { # First, let's unpack
63+ if ($io [$key ] -is [PSVariable ]) { $psvariable.set ($io [$key ]) }
64+ else { $psvariable.set ($key , $io [$key ]) }
65+ } # and then declare a few filters to make code more readable.
66+ filter outputError ([int ]$Number ) {
67+ $reply.StatusCode = $Number ; $reply.Close (); continue nextRequest
68+ }
69+ filter outputHeader {
70+ $reply.Length = $file.Length ; $reply.Close (); continue nextRequest
71+ }
72+ filter outputFile {
73+ $reply.ContentType = $contentTypes [$potentialPath ]
74+ $fileStream = $file.OpenRead ()
75+ $fileStream.CopyTo ($reply.OutputStream )
76+ $fileStream.Close (); $fileStream.Dispose (); $reply.Close ()
77+ continue nextRequest
78+ }
79+ # Listen for the next request
80+ :nextRequest while ($httpListener.IsListening ) {
81+ $getContext = $httpListener.GetContextAsync ()
82+ while (-not $getContext.Wait (17 )) { }
83+ $request , $reply = # and reply to it.
84+ $getContext.Result.Request , $getContext.Result.Response
85+ $method , $localPath =
86+ $request.HttpMethod , $request.Url.LocalPath
87+ # If the method is not allowed, output error 405
88+ if ($method -notin ' get' , ' head' ) { outputError 405 }
89+ # If the file does not exist, output error 404
90+ if (-not ($files -and $files [$localPath ])) { outputError 404 }
91+ $file = $files [$request.Url.LocalPath ]
92+ # If they asked for header information, output it.
93+ if ($request.httpMethod -eq ' head' ) { outputHeader }
94+ outputFile # otherwise, output the file.
95+ }
96+ } - ThrottleLimit 100 - ArgumentList $IO - Name " $RootUrl " | # Output our job,
97+ Add-Member - NotePropertyMembers @ { # but attach a few properties first:
98+ HttpListener = $httpListener # * The listener (so we can stop it)
99+ IO = $IO # * The IO (so we can change it)
100+ Url = " $RootUrl " # The URL (so we can easily access it).
101+ } - Force - PassThru # Pass all of that thru and return it to you.
0 commit comments