Servers/Server101.ps1
|
<# .SYNOPSIS Server 101 .DESCRIPTION Server 101: A file server in 101 lines of pure PowerShell. .EXAMPLE ./Server101.ps1 ($pwd | Split-Path) #> param( <# The Root Directory. #> [string]$RootDirectory = $PSScriptRoot, # The rootUrl of the server. By default, a random loopback address. [string]$RootUrl= "http://127.0.0.1:$(Get-Random -Minimum 4200 -Maximum 42000)/", # 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" ".json" = "application/json"; ".xml" = "application/xml" ; ".js" = "text/javascript" ; ".jsm" = "text/javascript" ; ".ps1" = "text/x-powershell" }) $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]@{} } # Then map each file into one or more /uris foreach ($file in Get-ChildItem -File -Path $RootDirectory -Recurse) { $relativePath = $file.FullName.Substring($RootDirectory.Length) -replace '[\\/]', '/' $fileUris = @($relativePath) + @( 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) { $io.ContentTypes[$fileUri] = # and map content types now $TypeMap[$file.Extension] ? # so we don't have to later. $TypeMap[$file.Extension] : 'text/plain' $io.Files[$fileUri] = $file } } # 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 } filter outputHeader { $reply.Length=$file.Length; $reply.Close(); continue nextRequest } filter outputFile { $reply.ContentType = $contentTypes[$potentialPath] $fileStream = $file.OpenRead() $fileStream.CopyTo($reply.OutputStream) $fileStream.Close(); $fileStream.Dispose(); $reply.Close() continue nextRequest } # Listen for the next request :nextRequest 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 # 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[$request.Url.LocalPath] # If they asked for header information, output it. if ($request.httpMethod -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). } -Force -PassThru # Pass all of that thru and return it to you. |