# 对 NativeAOT 发布产物做端到端冒烟:创建频道、加入、追加帧、读日志、WebSocket 推送、GATED 下载、删除。 # 用法: pwsh scripts/smoke.ps1 -Exe out/win-x64/webdrop.exe param( [Parameter(Mandatory)][string]$Exe, [int]$Port = 5188 ) $ErrorActionPreference = 'Stop' $base = "http://127.0.0.1:$Port" $dataDir = Join-Path ([IO.Path]::GetTempPath()) ("webdrop-smoke-" + [guid]::NewGuid().ToString('N')) function B64([byte[]]$b) { [Convert]::ToBase64String($b).TrimEnd('=').Replace('+', '-').Replace('/', '_') } function Rand([int]$n) { $b = [byte[]]::new($n); [Security.Cryptography.RandomNumberGenerator]::Fill($b); $b } function Sha([byte[]]$b) { [Security.Cryptography.SHA256]::HashData($b) } function Check($cond, $msg) { if (-not $cond) { throw "冒烟失败: $msg" } else { Write-Host " ok $msg" } } function Frame([byte[]]$item, [uint32]$index, [byte]$flags, [int]$payload, [byte]$fill) { $f = [byte[]]::new(26 + $payload) $f[0] = 1; $f[1] = $flags [Array]::Copy($item, 0, $f, 2, 16) $idx = [BitConverter]::GetBytes($index); [Array]::Reverse($idx); [Array]::Copy($idx, 0, $f, 18, 4) $len = [BitConverter]::GetBytes([uint32]$payload); [Array]::Reverse($len); [Array]::Copy($len, 0, $f, 22, 4) for ($i = 26; $i -lt $f.Length; $i++) { $f[$i] = $fill } , $f } function Headers($role, $token, [byte[]]$secret) { $h = @{ 'X-WD-Role' = $role; 'X-WD-Endpoint' = (B64 $secret) } if ($token) { $h['Authorization'] = 'Bearer ' + (B64 $token) } $h } $env:WebDrop__DataDir = $dataDir $proc = Start-Process -FilePath $Exe -ArgumentList '--urls', $base -PassThru -WindowStyle Hidden try { for ($i = 0; $i -lt 50; $i++) { try { $null = Invoke-WebRequest "$base/api/v1/info" -UseBasicParsing; break } catch { Start-Sleep -Milliseconds 200 } } $info = Invoke-RestMethod "$base/api/v1/info" Check ($info.protocol -eq 1 -and -not $info.captcha.enabled) 'info 可访问,默认不启用验证码' $id = Rand 16; $readTok = Rand 32; $writeTok = Rand 32; $ownerTok = Rand 32 $ownerSecret = Rand 32; $memberSecret = Rand 32 $body = @{ id = (B64 $id); mode = 'enc'; read_hash = (B64 (Sha $readTok)); write_hash = (B64 (Sha $writeTok)); owner_hash = (B64 (Sha $ownerTok)) } | ConvertTo-Json $ownerH = Headers 'owner' $ownerTok $ownerSecret $created = Invoke-RestMethod "$base/api/v1/channels" -Method Post -Body $body -ContentType 'application/json' -Headers $ownerH Check ($created.id -eq (B64 $id) -and $created.seats_used -eq 1) '创建频道成功' $cid = B64 $id $memberH = Headers 'write' $writeTok $memberSecret $joined = Invoke-RestMethod "$base/api/v1/channels/$cid/seats" -Method Post -Headers $memberH Check ($joined.seats_used -eq 2) '成员加入' # enc 帧:负载 = 1024 档位 + 16 标签 $item = Rand 16 $frame = Frame $item 0 9 1040 0x42 $r1 = Invoke-WebRequest "$base/api/v1/channels/$cid/frames" -Method Post -Body $frame -ContentType 'application/octet-stream' -Headers $ownerH -UseBasicParsing Check ($r1.StatusCode -eq 201) '追加帧返回 201' $r2 = Invoke-WebRequest "$base/api/v1/channels/$cid/frames" -Method Post -Body $frame -ContentType 'application/octet-stream' -Headers $ownerH -UseBasicParsing Check ($r2.StatusCode -eq 200) '幂等重发返回 200' $log = Invoke-WebRequest "$base/api/v1/channels/$cid/log" -Headers $memberH -UseBasicParsing $bytes = [byte[]]$log.Content Check ($bytes.Length -eq 9 + $frame.Length -and $bytes[8] -eq 1) '日志返回完整的帧记录' Check ((Compare-Object $bytes[9..($bytes.Length - 1)] $frame) -eq $null) '帧字节与上传一致' # WebSocket:凭据走子协议;先收到历史,再收到实时追加 $ws = [Net.WebSockets.ClientWebSocket]::new() foreach ($p in 'wd.v1', 'wd.role.write', ('wd.auth.' + (B64 $writeTok)), ('wd.ep.' + (B64 $memberSecret))) { $ws.Options.AddSubProtocol($p) } $ws.ConnectAsync([Uri]("ws://127.0.0.1:$Port/api/v1/channels/$cid/ws?from=0"), [Threading.CancellationToken]::None).Wait() $seg = [ArraySegment[byte]]::new([byte[]]::new(4096)) $r = $ws.ReceiveAsync($seg, [Threading.CancellationToken]::None).Result Check ($r.Count -eq 9 + $frame.Length) 'WebSocket 收到历史记录' $item2 = Rand 16 $null = Invoke-WebRequest "$base/api/v1/channels/$cid/frames" -Method Post -Body (Frame $item2 0 9 1040 0x43) -ContentType 'application/octet-stream' -Headers $ownerH -UseBasicParsing $r = $ws.ReceiveAsync($seg, [Threading.CancellationToken]::None).Result Check ($r.Count -eq 9 + 26 + 1040) 'WebSocket 实时收到新帧' $ws.Dispose() # GATED 条目:三帧,下载上限 1 $big = Rand 16 foreach ($i in 0..2) { $flags = if ($i -eq 2) { 13 } else { 12 } # Gated|Enc (+Final) $h = $ownerH + @{ 'X-WD-Max-Downloads' = '1' } $null = Invoke-WebRequest "$base/api/v1/channels/$cid/frames" -Method Post -Body (Frame $big ([uint32]$i) $flags 1040 ($i + 1)) -ContentType 'application/octet-stream' -Headers $h -UseBasicParsing } $big64 = B64 $big $dl = Invoke-WebRequest "$base/api/v1/channels/$cid/items/$big64/frames" -Headers $memberH -UseBasicParsing Check (([byte[]]$dl.Content).Length -eq 3 * (26 + 1040)) 'GATED 条目下载得到全部三帧' $gone = try { Invoke-WebRequest "$base/api/v1/channels/$cid/items/$big64/frames" -Headers $memberH -UseBasicParsing; 0 } catch { [int]$_.Exception.Response.StatusCode } Check ($gone -eq 410) '下载上限生效,第二次返回 410' $unauth = try { Invoke-WebRequest "$base/api/v1/channels/$cid" -Headers (Headers 'owner' (Rand 32) $ownerSecret) -UseBasicParsing; 0 } catch { [int]$_.Exception.Response.StatusCode } Check ($unauth -eq 401) '错误令牌返回 401' $del = Invoke-WebRequest "$base/api/v1/channels/$cid" -Method Delete -Headers $ownerH -UseBasicParsing Check ($del.StatusCode -eq 204) '删除频道' # 数据目录(含 SQLite 的 -wal 文件)里不应出现令牌或终端密钥的原值;服务仍在运行,用共享读取打开 $found = $false foreach ($file in (Get-ChildItem $dataDir -Recurse -File)) { $fs = [IO.File]::Open($file.FullName, 'Open', 'Read', 'ReadWrite') try { $buf = [byte[]]::new($fs.Length); $null = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() } $content = [Text.Encoding]::Latin1.GetString($buf) if ($content.Contains((B64 $ownerTok)) -or $content.Contains((B64 $ownerSecret)) -or $content.Contains((B64 $writeTok))) { $found = $true } } Check (-not $found) '数据目录里没有令牌或终端密钥的原值' Write-Host "`n冒烟通过" -ForegroundColor Green } finally { if (-not $proc.HasExited) { Stop-Process $proc -Force } Start-Sleep -Milliseconds 300 Remove-Item -Recurse -Force $dataDir -ErrorAction SilentlyContinue }