<# .SYNOPSIS Minimal WebDAV server for testing deploy-generator.ps1 without a real host. .DESCRIPTION Serves files under -Root at http://localhost:/, requiring Basic authentication with -User/-Pass. Supports GET, PUT, DELETE, COPY and MOVE (with the Destination and Overwrite headers) - the subset the deploy script uses. Every authenticated request's method and path is appended to -LogFile so tests can assert on the exact sequence. GET requests under /public/ need no authentication, standing in for the public web server. Requesting /__shutdown stops the server. If a file named .mock-cpanel exists in -Root, successful COPY and MOVE answer 200 instead of 201/204, as cPanel's Web Disk (cpdavd) does. #> param( [Parameter(Mandatory)] [int] $Port, [Parameter(Mandatory)] [string] $Root, [Parameter(Mandatory)] [string] $User, [Parameter(Mandatory)] [string] $Pass, [Parameter(Mandatory)] [string] $LogFile ) $ErrorActionPreference = 'Stop' $expected = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${User}:${Pass}")) $listener = New-Object System.Net.HttpListener $listener.Prefixes.Add("http://localhost:$Port/") $listener.Start() <# .SYNOPSIS Maps a request path (or absolute Destination URL) to a file under Root. #> function Resolve-Local([string] $UrlOrPath) { $path = if ($UrlOrPath -match '^[a-z]+://') { ([Uri]$UrlOrPath).AbsolutePath } else { $UrlOrPath } $rel = [Uri]::UnescapeDataString($path).TrimStart('/').Replace('/', '\') return Join-Path $Root $rel } try { while ($listener.IsListening) { $ctx = $listener.GetContext() $req = $ctx.Request $res = $ctx.Response try { if ($req.Url.AbsolutePath -eq '/__shutdown') { $res.StatusCode = 200 $res.Close() break } # /public/ stands in for the web server: readable without credentials. $isPublic = $req.HttpMethod -eq 'GET' -and $req.Url.AbsolutePath.StartsWith('/public/') if (-not $isPublic -and $req.Headers['Authorization'] -ne $expected) { $res.StatusCode = 401 $res.AddHeader('WWW-Authenticate', 'Basic realm="mock"') continue } Add-Content -LiteralPath $LogFile -Value ("{0} {1}" -f $req.HttpMethod, [Uri]::UnescapeDataString($req.Url.AbsolutePath)) $file = Resolve-Local $req.Url.AbsolutePath switch ($req.HttpMethod) { 'GET' { if (Test-Path -LiteralPath $file -PathType Leaf) { $bytes = [IO.File]::ReadAllBytes($file) $res.StatusCode = 200 $res.OutputStream.Write($bytes, 0, $bytes.Length) } else { $res.StatusCode = 404 } } 'PUT' { $ms = New-Object IO.MemoryStream $req.InputStream.CopyTo($ms) $existed = Test-Path -LiteralPath $file -PathType Leaf New-Item -ItemType Directory -Force -Path (Split-Path $file) | Out-Null [IO.File]::WriteAllBytes($file, $ms.ToArray()) $res.StatusCode = if ($existed) { 204 } else { 201 } } 'DELETE' { if (Test-Path -LiteralPath $file -PathType Leaf) { Remove-Item -LiteralPath $file $res.StatusCode = 204 } else { $res.StatusCode = 404 } } { $_ -in 'COPY', 'MOVE' } { $destHeader = $req.Headers['Destination'] if (-not $destHeader) { $res.StatusCode = 400; break } $dest = Resolve-Local $destHeader $overwrite = $req.Headers['Overwrite'] -ne 'F' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { $res.StatusCode = 404; break } $existed = Test-Path -LiteralPath $dest -PathType Leaf if ($existed -and -not $overwrite) { $res.StatusCode = 412; break } New-Item -ItemType Directory -Force -Path (Split-Path $dest) | Out-Null if ($req.HttpMethod -eq 'COPY') { Copy-Item -LiteralPath $file -Destination $dest -Force } else { Move-Item -LiteralPath $file -Destination $dest -Force } $res.StatusCode = if ($existed) { 204 } else { 201 } # cPanel's cpdavd answers a successful COPY/MOVE with 200; # a test opts into that by creating .mock-cpanel in Root. if (Test-Path -LiteralPath (Join-Path $Root '.mock-cpanel')) { $res.StatusCode = 200 } } default { $res.StatusCode = 405 } } } finally { try { $res.Close() } catch { } } } } finally { $listener.Stop() $listener.Close() }