# Simple static file server for the wedding site (Windows, no Node/Python required) # Listens on localhost + LAN IPs so phones on the same WiFi can open the site. $ErrorActionPreference = "Stop" $root = $PSScriptRoot $port = 8080 $sitePath = "/www.mewedding.vn/index.html" $mime = @{ ".html" = "text/html; charset=utf-8" ".htm" = "text/html; charset=utf-8" ".css" = "text/css; charset=utf-8" ".js" = "application/javascript; charset=utf-8" ".json" = "application/json; charset=utf-8" ".png" = "image/png" ".jpg" = "image/jpeg" ".jpeg" = "image/jpeg" ".gif" = "image/gif" ".svg" = "image/svg+xml" ".webp" = "image/webp" ".woff" = "font/woff" ".woff2" = "font/woff2" ".ttf" = "font/ttf" ".otf" = "font/otf" ".mp3" = "audio/mpeg" ".mp4" = "video/mp4" ".ico" = "image/x-icon" } function Get-ContentType([string]$path) { $ext = [System.IO.Path]::GetExtension($path).ToLowerInvariant() if ($mime.ContainsKey($ext)) { return $mime[$ext] } return "application/octet-stream" } function Send-File([System.Net.HttpListenerContext]$ctx, [string]$filePath, [string]$extraContentType = $null) { $bytes = if ($extraContentType -eq 'text/html') { $text = [System.IO.File]::ReadAllText($filePath, [Text.UTF8Encoding]::new($false)) if ($text -notmatch '', ('' + "`n ") } [Text.Encoding]::UTF8.GetBytes($text) } else { [System.IO.File]::ReadAllBytes($filePath) } $ctx.Response.ContentType = if ($extraContentType) { $extraContentType } else { Get-ContentType $filePath } $ctx.Response.Headers.Add('Cache-Control', 'public, max-age=3600') $ctx.Response.ContentLength64 = $bytes.Length $ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length) } function Resolve-StaticFile([string]$root, [string]$rawPath) { $rel = [System.Uri]::UnescapeDataString($rawPath).TrimStart('/').Replace('/', [IO.Path]::DirectorySeparatorChar) $candidates = @( $rel, ($rel -replace '%20', ' '), ($rel -replace '\\ ', ' ') ) | Select-Object -Unique $rootFull = [IO.Path]::GetFullPath($root) foreach ($c in $candidates) { if ([string]::IsNullOrWhiteSpace($c)) { continue } $fp = [IO.Path]::GetFullPath((Join-Path $root $c)) if ($fp.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path $fp -PathType Leaf)) { return $fp } } # Tim file theo ten (ho tro ten co dau / khoang trang khac encoding) $leaf = Split-Path $rel -Leaf $parent = Split-Path (Join-Path $root $rel) -Parent if ($leaf -and (Test-Path $parent -PathType Container)) { $decodedLeaf = [System.Uri]::UnescapeDataString($leaf.Replace('+', ' ')) $hit = Get-ChildItem -LiteralPath $parent -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $decodedLeaf -or $_.Name -eq $leaf } | Select-Object -First 1 if ($hit) { return $hit.FullName } } return $null } function Test-ValidLanIPv4([string]$ip) { return $ip -match '^\d{1,3}(\.\d{1,3}){3}$' -and $ip -notmatch '^127\.' -and $ip -notmatch '^169\.254\.' } function Get-LanIPv4Addresses { $rows = @() try { $rows = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { Test-ValidLanIPv4 $_.IPAddress }) } catch { $hostEntry = [System.Net.Dns]::GetHostEntry([System.Net.Dns]::GetHostName()) foreach ($a in $hostEntry.AddressList) { if ($a.AddressFamily -eq 'InterNetwork') { $ip = $a.ToString() if (Test-ValidLanIPv4 $ip) { $rows += [PSCustomObject]@{ IPAddress = $ip; InterfaceAlias = 'Unknown' } } } } } # Bo VPN / adapter ao $rows = @($rows | Where-Object { $_.InterfaceAlias -notmatch 'VPN|OpenVPN|TAP|TUN|Hyper-V|Virtual|Loopback|vEthernet' -and $_.InterfaceAlias -notmatch 'Connection\s*\*' }) $wifi = @($rows | Where-Object { $_.InterfaceAlias -match 'Wi-?Fi' } | ForEach-Object { $_.IPAddress } | Select-Object -Unique) $eth = @($rows | Where-Object { $_.InterfaceAlias -match 'Ethernet' -and $_.InterfaceAlias -notmatch 'Virtual' } | ForEach-Object { $_.IPAddress } | Select-Object -Unique) $other = @($rows | ForEach-Object { $_.IPAddress } | Where-Object { $_ -notin $wifi -and $_ -notin $eth } | Select-Object -Unique) return @($wifi + $eth + $other | Select-Object -Unique) } function Get-WifiIPv4 { $all = @(Get-LanIPv4Addresses) return ($all | Select-Object -First 1) } function Start-HttpServer([int]$port) { $wildcard = "http://+:${port}/" $listener = New-Object System.Net.HttpListener $null = $listener.Prefixes.Add($wildcard) try { $listener.Start() return @{ Listener = $listener; Mode = 'wildcard' } } catch { if ($_.Exception.Message -match 'conflict|already') { throw [System.InvalidOperationException]::new( 'PORT_BUSY: Dong cua so PowerShell dang chay start.bat (Ctrl+C) roi chay lai.') } throw } } $lanIps = @(Get-LanIPv4Addresses) $fwOk = @(Get-NetFirewallRule -DisplayName "Wedding Site LAN $port" -ErrorAction SilentlyContinue | Where-Object { $_.Enabled -eq 'True' }).Count -gt 0 $bindMode = $null $listener = $null try { $started = Start-HttpServer $port $listener = $started.Listener $bindMode = $started.Mode } catch { Write-Host " Loi khoi dong server: $($_.Exception.Message)" -ForegroundColor Red Write-Host " Dong server cu (Ctrl+C) hoac: Stop-Process -Name powershell -Force (neu bi ket)" -ForegroundColor Yellow Write-Host " Neu can: PowerShell (Admin) -> .\setup-wifi.ps1" -ForegroundColor Yellow exit 1 } $localUrl = "http://localhost:${port}${sitePath}" $wifiIp = Get-WifiIPv4 Write-Host '' Write-Host ' Thiep cuoi dang chay:' -ForegroundColor Green Write-Host " May tinh: $localUrl" -ForegroundColor Cyan if ($wifiIp) { $phoneUrl = "http://${wifiIp}:${port}${sitePath}" Write-Host '' Write-Host ' >>> DIEN THOAI (cung WiFi) - copy URL nay:' -ForegroundColor Yellow Write-Host " $phoneUrl" -ForegroundColor Green } if ($lanIps.Count -gt 1) { Write-Host '' Write-Host ' IP khac (chi dung neu URL tren khong vao):' -ForegroundColor DarkGray foreach ($ip in $lanIps) { if ($ip -ne $wifiIp) { Write-Host " http://${ip}:${port}${sitePath}" -ForegroundColor DarkGray } } } elseif (-not $wifiIp) { Write-Host '' Write-Host ' Khong tim thay IP WiFi. Ket noi WiFi roi chay lai start.bat.' -ForegroundColor Yellow } if (-not $fwOk) { Write-Host '' Write-Host ' !!! CHUA MO FIREWALL - dien thoai thuong KHONG vao duoc !!!' -ForegroundColor Red Write-Host ' Chuot phai setup-wifi.ps1 -> Run as administrator (hoac mo-firewall.bat)' -ForegroundColor Yellow } Write-Host '' Write-Host ' Dien thoai khong vao? Chay lai setup-wifi.ps1 (Admin) de mo firewall.' -ForegroundColor DarkGray Write-Host ' Tat VPN tren may tinh/dien thoai. Router tat AP isolation neu co.' -ForegroundColor DarkGray Write-Host ' Nhan Ctrl+C de dung server.' -ForegroundColor DarkGray Write-Host '' try { Start-Process $localUrl | Out-Null } catch { Write-Host " Mo trinh duyet thu cong: $localUrl" -ForegroundColor Yellow } while ($listener.IsListening) { $context = $listener.GetContext() $request = $context.Request $response = $context.Response $client = $request.RemoteEndPoint.Address.ToString() Write-Host " [$((Get-Date).ToString('HH:mm:ss'))] $client -> $($request.Url.LocalPath)" -ForegroundColor DarkGray try { $rawPath = $request.Url.LocalPath if ([string]::IsNullOrWhiteSpace($rawPath) -or $rawPath -eq "/") { $rawPath = $sitePath } $rootFull = [IO.Path]::GetFullPath($root) $filePath = Resolve-StaticFile $root $rawPath if (-not $filePath) { $dirRel = [System.Uri]::UnescapeDataString($rawPath).TrimStart('/').Replace('/', [IO.Path]::DirectorySeparatorChar) $dirPath = [IO.Path]::GetFullPath((Join-Path $root $dirRel)) if ($dirPath.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path $dirPath -PathType Container)) { $index = Join-Path $dirPath "index.html" if (Test-Path $index) { $filePath = $index } } } if (-not $filePath) { $response.StatusCode = 404 Write-Host " [404] $rawPath" -ForegroundColor Red $buf = [Text.Encoding]::UTF8.GetBytes("404 Not Found: $rawPath") $response.OutputStream.Write($buf, 0, $buf.Length) continue } $asHtml = $filePath.EndsWith('index.html', [StringComparison]::OrdinalIgnoreCase) Send-File $context $filePath $(if ($asHtml) { 'text/html' } else { $null }) } catch { $response.StatusCode = 500 $msg = [Text.Encoding]::UTF8.GetBytes($_.Exception.Message) $response.OutputStream.Write($msg, 0, $msg.Length) } finally { $response.Close() } }