247 lines
9.1 KiB
PowerShell
247 lines
9.1 KiB
PowerShell
# 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 '<base\s') {
|
|
$text = $text -replace '<head>', ('<head>' + "`n <base href=`"/`">")
|
|
}
|
|
[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()
|
|
}
|
|
}
|