├── Private └── Router.ps1 ├── README.md ├── Routes.ps1 ├── Start-WebListener.ps1 ├── WebListener.psd1 ├── WebListener.psm1 └── views ├── errorpages └── 404.html ├── favicon.png └── index.html /Private/Router.ps1: -------------------------------------------------------------------------------- 1 | function Router { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory=$true,Position=0)][String]$RequestType, 5 | [Parameter(Mandatory=$true,Position=1)][String]$RequestURL 6 | ) 7 | 8 | . $Root\Routes.ps1 9 | $Route = ($Routes | Where-Object {$_.RequestType -eq $RequestType -and $_.RequestURL -eq $RequestURL}) 10 | "RequestType: " + $RequestType | Write-Debug 11 | # GET 12 | if ($RequestType -eq 'GET') { 13 | 14 | if (Test-Path -PathType Leaf -Path "$Root\views$RequestURL") { 15 | 16 | if ($($Route.RequestURL) -notmatch 'favicon') { 17 | Write-Verbose "Page found: $RequestURL" 18 | $Response.StatusCode = 200 19 | } 20 | 21 | $PageContent = Get-Content ("$Root\views$RequestURL") 22 | 23 | } elseif ($Route.RedirectURL) { 24 | 25 | Write-Verbose "Page found: $($Route.RedirectURL)" 26 | $Response.StatusCode = 200 27 | $PageContent = Get-Content ("$Root\views$($Route.RedirectURL)") 28 | 29 | } elseif ($Route.ScriptBlock) { 30 | 31 | Write-Verbose "Running ScriptBlock" 32 | $PageContent = & ( [ScriptBlock]::Create($Route.ScriptBlock) ) 33 | 34 | if ($PageContent) { 35 | $Response.StatusCode = 200 36 | } else { 37 | $Response.StatusCode = 500 38 | $PageContent = Get-Content ("$Root\views\errorpages\500.html") 39 | } 40 | 41 | } else { 42 | 43 | Write-Verbose "Page not found: 404: $RequestURL" 44 | $Response.StatusCode = 404 45 | $PageContent = Get-Content ("$Root\views\errorpages\404.html") 46 | 47 | } 48 | $ResponseBuffer = [System.Text.Encoding]::UTF8.GetBytes($PageContent) 49 | "Response " + $Response.StatusCode + " with Length $($ResponseBuffer.Length)" | Write-Debug 50 | $Response.ContentLength64 = $ResponseBuffer.Length 51 | $Response.OutputStream.Write($ResponseBuffer,0,$ResponseBuffer.Length) 52 | $Response.Close() 53 | 54 | } 55 | 56 | # POST 57 | if ($RequestType -eq 'POST') { 58 | Write-Output $Context.Request 59 | } 60 | 61 | # PUT 62 | if ($RequestType -eq 'PUT') { 63 | 64 | } 65 | 66 | # DELETE 67 | if ($RequestType -eq 'DELETE') { 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebListener 2 | WebListener is a small, PowerShell-based web server. It was primarily designed for really basic, lightweight use, and for testing simple web apps. One advantage to this is that it's really easy to create a frontend that will allow you to run PowerShell code on the backend, such as a page to display server statistics, or a portal to deploy virtual machines. 3 | 4 | ## Getting Started 5 | To get started, simply import the module, then run Start-WebListener. 6 | 7 | ``` 8 | Start-WebListener 9 | ``` 10 | 11 | Your web server is now live at *http://localhost:8080* ! 12 | 13 | Your web pages and supporting folder structure should live in the '*views*' directory. 14 | 15 | ## Routing 16 | 17 | WebListener has a simple routing mechanism that will help you to control how requests are handled. 18 | 19 | To add a route, open up '*Routes.ps1*', and add a new hashtable to the array. Your hashtable should look like this: 20 | 21 | ``` 22 | @{ 23 | 'RequestType' = 'GET' 24 | 'RequestURL' = '/' 25 | 'RedirectURL' = '/index.html' 26 | 'ScriptBlock = {} 27 | } 28 | ``` 29 | 30 | ***RequestType*** is mandatory. This key should contain one of the following four values: GET, PUT, POST, DELETE. 31 | 32 | ***RequestURL*** is the URL that the router will respond to. For example, to create a route for /index, enter '/index'. 33 | 34 | ***RedirectURL*** is the full path to the file that will be served when a request for the RequestURL is made. If the filepath is the same as the RequestURL, this can be left blank or excluded. 35 | 36 | ***ScriptBlock*** allows you to run PowerShell code when a URL is requested. If you don't want to run anything, leave this blank, or exclude it. 37 | 38 | In the above example, when a call is made to *http://localhost:8080/*, the user will be redirected to '*index.html*'. -------------------------------------------------------------------------------- /Routes.ps1: -------------------------------------------------------------------------------- 1 | $Routes = @( 2 | @{ 3 | 'RequestType' = 'GET' 4 | 'RequestURL' = '/index' 5 | 'RedirectURL' = '/index.html' 6 | } 7 | 8 | @{ 9 | 'RequestType' = 'GET' 10 | 'RequestURL' = '' 11 | 'RedirectURL' = '/index.html' 12 | } 13 | 14 | @{ 15 | 'RequestType' = 'GET' 16 | 'RequestURL' = '/' 17 | 'RedirectURL' = '/index.html' 18 | } 19 | 20 | # Example of a route that contains a custom scriptblock 21 | @{ 22 | 'RequestType' = 'GET' 23 | 'RequestURL' = '/process' 24 | 'ScriptBlock' = { 25 | Get-Process 26 | } 27 | } 28 | 29 | # Example of a more web-friendly route with scriptblock 30 | @{ 31 | 'RequestType' = 'GET' 32 | 'RequestURL' = '/getservice' 33 | 'ScriptBlock' = { 34 | Get-Service | ConvertTo-Html 35 | } 36 | } 37 | 38 | # Get-Process piped to ConvertTo-Html will generate a table with all process data, so it is better to filter it before output 39 | @{ 40 | 'RequestType' = 'GET' 41 | 'RequestURL' = '/getprocess' 42 | 'ScriptBlock' = { 43 | Get-Process | select Name, Id, CPU, WorkingSet, Path | ConvertTo-Html 44 | } 45 | } 46 | 47 | # Example of try .. catch block 48 | @{ 49 | 'RequestType' = 'GET' 50 | 'RequestURL' = '/getfail' 51 | 'ScriptBlock' = { 52 | try { 53 | Get-ChildItem c:\..\.. | ConvertTo-Html 54 | 55 | } 56 | catch { 57 | $Error[0] | ConvertTo-Html -as List 58 | } 59 | 60 | } 61 | } 62 | 63 | # ... Why not? 64 | @{ 65 | 'RequestType' = 'GET' 66 | 'RequestURL' = '/reloadroutes' 67 | 'ScriptBlock' = { 68 | try { 69 | . $Root\Routes.ps1 70 | ConvertTo-Html -Body 'Reload is complete.' 71 | } 72 | catch { 73 | $Error[0] | ConvertTo-Html -as List 74 | } 75 | 76 | } 77 | } 78 | ) 79 | -------------------------------------------------------------------------------- /Start-WebListener.ps1: -------------------------------------------------------------------------------- 1 | function Start-WebListener { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory=$false, HelpMessage="HTTP or HTTPS?")][ValidateSet("http","https")][String]$Protocol, 5 | [Parameter(Mandatory=$false)][String][Alias('Hostname')]$IPAddress, 6 | [Parameter(Mandatory=$false)][Int]$Port 7 | ) 8 | 9 | BEGIN { 10 | # Load the Router 11 | . .\Private\Router.ps1 12 | 13 | if (!($Protocol)) { 14 | $Protocol = 'http' 15 | } 16 | if (!($IPAddress)) { 17 | $IPAddress = '127.0.0.1' 18 | } 19 | if (!($Port)) { 20 | $Port = 8080 21 | } 22 | $URL = "$Protocol"+"://$IPAddress"+":$Port/" 23 | $Root = Split-Path -Parent $PSCommandPath 24 | 25 | } 26 | 27 | PROCESS { 28 | 29 | # Spin up a new HTTP Listener 30 | $Listener = New-Object System.Net.HttpListener 31 | $Listener.Prefixes.Add($URL) 32 | $Listener.Start() 33 | Write-Output "Starting Listener at $URL..." 34 | $Exit = $False 35 | 36 | while ($Listener.IsListening) { 37 | 38 | # Use async request to avoid blocking the current thread 39 | $ContextRequest = $Listener.GetContextAsync() 40 | 41 | # Check every 30ms if a request has been received 42 | while($ContextRequest.IsCompleted -ne $True -and $ContextRequest.IsCanceled -ne $True -and $ContextRequest.IsFaulted -ne $True -and $Exit -ne $True) { 43 | [System.Threading.Thread]::Sleep(30) 44 | 45 | # Process Ctrl+C 46 | if($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character)) { 47 | Write-Output "Stopping Listener..." 48 | $Listener.Abort() 49 | Break 50 | } 51 | } 52 | 53 | # Request Handler 54 | $Context = $ContextRequest.Result 55 | $Request = $Context.Request 56 | $RequestType = $Request.HttpMethod 57 | $RequestURL = $Request.RawUrl 58 | 59 | if ($Context) { 60 | # Response Handler 61 | $Response = $Context.Response 62 | $Response.Headers.Add("Content-Type","text/html") 63 | 64 | # Let Router handle the logic 65 | Router $RequestType $RequestURL 66 | } 67 | 68 | } 69 | 70 | # Close the listener 71 | $Listener.Close() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /WebListener.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofonseca/WebListener/9402952d0a63656f3953e13defce6b2006e93616/WebListener.psd1 -------------------------------------------------------------------------------- /WebListener.psm1: -------------------------------------------------------------------------------- 1 | (Get-ChildItem '*.ps1') | ForEach-Object { 2 | . $_.FullName 3 | Export-ModuleMember -Function $_.BaseName 4 | } -------------------------------------------------------------------------------- /views/errorpages/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

404: Page not found.

4 | 5 | -------------------------------------------------------------------------------- /views/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cofonseca/WebListener/9402952d0a63656f3953e13defce6b2006e93616/views/favicon.png -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hello, World!

6 | 7 | --------------------------------------------------------------------------------