136 lines
5.2 KiB
PowerShell
136 lines
5.2 KiB
PowerShell
param(
|
|
[ValidateRange(1024, 65535)]
|
|
[int]$Port = 4173,
|
|
[switch]$NoBrowser
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
|
|
$rootWithSeparator = $projectRoot.TrimEnd([System.IO.Path]::DirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
|
$serverUrl = "http://127.0.0.1:$Port/"
|
|
$listener = [System.Net.HttpListener]::new()
|
|
$listener.Prefixes.Add($serverUrl)
|
|
|
|
$mimeTypes = @{
|
|
'.css' = 'text/css; charset=utf-8'
|
|
'.flac' = 'audio/flac'
|
|
'.html' = 'text/html; charset=utf-8'
|
|
'.ico' = 'image/x-icon'
|
|
'.jpeg' = 'image/jpeg'
|
|
'.jpg' = 'image/jpeg'
|
|
'.js' = 'text/javascript; charset=utf-8'
|
|
'.json' = 'application/json; charset=utf-8'
|
|
'.md' = 'text/markdown; charset=utf-8'
|
|
'.png' = 'image/png'
|
|
'.svg' = 'image/svg+xml'
|
|
'.wav' = 'audio/wav'
|
|
'.webp' = 'image/webp'
|
|
}
|
|
|
|
function Send-TextResponse {
|
|
param(
|
|
[System.Net.HttpListenerResponse]$Response,
|
|
[int]$StatusCode,
|
|
[string]$Message
|
|
)
|
|
|
|
$body = [System.Text.Encoding]::UTF8.GetBytes($Message)
|
|
$Response.StatusCode = $StatusCode
|
|
$Response.ContentType = 'text/plain; charset=utf-8'
|
|
$Response.ContentLength64 = $body.Length
|
|
$Response.OutputStream.Write($body, 0, $body.Length)
|
|
}
|
|
|
|
try {
|
|
try {
|
|
$listener.Start()
|
|
}
|
|
catch [System.Net.HttpListenerException] {
|
|
throw [System.InvalidOperationException]::new(
|
|
"Port $Port is unavailable. Close the running micarrsim window or choose another port, for example: start.cmd -Port 4174",
|
|
$_.Exception
|
|
)
|
|
}
|
|
Write-Host "micarrsim is running at $serverUrl" -ForegroundColor Green
|
|
Write-Host 'Press Ctrl+C to stop.' -ForegroundColor DarkGray
|
|
|
|
if (-not $NoBrowser) {
|
|
Start-Process $serverUrl
|
|
}
|
|
|
|
while ($listener.IsListening) {
|
|
$context = $listener.GetContext()
|
|
|
|
try {
|
|
$context.Response.Headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; media-src 'self' blob:; worker-src 'self'; connect-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'none'"
|
|
$context.Response.Headers['Referrer-Policy'] = 'no-referrer'
|
|
$context.Response.Headers['X-Content-Type-Options'] = 'nosniff'
|
|
|
|
if ($context.Request.HttpMethod -notin @('GET', 'HEAD')) {
|
|
$context.Response.Headers['Allow'] = 'GET, HEAD'
|
|
Send-TextResponse -Response $context.Response -StatusCode 405 -Message 'Method not allowed'
|
|
continue
|
|
}
|
|
|
|
$relativePath = [System.Uri]::UnescapeDataString($context.Request.Url.AbsolutePath.TrimStart('/'))
|
|
$relativePath = $relativePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar)
|
|
if ([string]::IsNullOrWhiteSpace($relativePath)) {
|
|
$relativePath = 'index.html'
|
|
}
|
|
$pathSegments = $relativePath -split '[\\/]'
|
|
if ($pathSegments | Where-Object { $_.StartsWith('.') }) {
|
|
Send-TextResponse -Response $context.Response -StatusCode 403 -Message 'Forbidden'
|
|
continue
|
|
}
|
|
|
|
$candidatePath = [System.IO.Path]::GetFullPath((Join-Path $projectRoot $relativePath))
|
|
$isWithinRoot = $candidatePath.Equals($projectRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
|
$candidatePath.StartsWith($rootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)
|
|
|
|
if (-not $isWithinRoot) {
|
|
Send-TextResponse -Response $context.Response -StatusCode 403 -Message 'Forbidden'
|
|
continue
|
|
}
|
|
|
|
if ([System.IO.Directory]::Exists($candidatePath)) {
|
|
$candidatePath = Join-Path $candidatePath 'index.html'
|
|
}
|
|
|
|
if (-not [System.IO.File]::Exists($candidatePath)) {
|
|
Send-TextResponse -Response $context.Response -StatusCode 404 -Message 'Not found'
|
|
continue
|
|
}
|
|
|
|
$content = [System.IO.File]::ReadAllBytes($candidatePath)
|
|
$extension = [System.IO.Path]::GetExtension($candidatePath).ToLowerInvariant()
|
|
$contentType = $mimeTypes[$extension]
|
|
if ([string]::IsNullOrWhiteSpace($contentType)) {
|
|
$contentType = 'application/octet-stream'
|
|
}
|
|
|
|
$context.Response.StatusCode = 200
|
|
$context.Response.ContentType = $contentType
|
|
$context.Response.ContentLength64 = $content.Length
|
|
$context.Response.Headers['Cache-Control'] = 'no-store'
|
|
if ($context.Request.HttpMethod -ne 'HEAD') {
|
|
$context.Response.OutputStream.Write($content, 0, $content.Length)
|
|
}
|
|
}
|
|
catch {
|
|
if ($context.Response.OutputStream.CanWrite) {
|
|
Send-TextResponse -Response $context.Response -StatusCode 500 -Message 'Internal server error'
|
|
}
|
|
Write-Warning $_
|
|
}
|
|
finally {
|
|
$context.Response.OutputStream.Close()
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
if ($listener.IsListening) {
|
|
$listener.Stop()
|
|
}
|
|
$listener.Close()
|
|
}
|