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() }