261 lines
11 KiB
PowerShell
261 lines
11 KiB
PowerShell
param(
|
|
[string]$BackendApiBase = "http://192.168.200.61:8080/api/v1",
|
|
[string]$BackendWsBase = "ws://192.168.200.61:8080/api/v1/gateway/ws",
|
|
[string]$DockerSshAlias = "docker-test",
|
|
[string]$WorkerContainerName = "rap_worker_smoke",
|
|
[string]$WorkerImage = "rap-rdp-worker:stage5-drive-visible",
|
|
[string]$ResourceName = "Windows Smoke Default Resource",
|
|
[string]$Email = "windows-smoke@example.local",
|
|
[string]$Password = "SmokePass!123",
|
|
[string]$OutputFrame = "artifacts/stage5-drive-visibility-frame.bmp"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Write-SmokeLog([string]$Message) {
|
|
Write-Host "[$(Get-Date -Format o)] $Message"
|
|
}
|
|
|
|
function Invoke-RemoteSqlScalar([string]$Sql) {
|
|
$encoded = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Sql))
|
|
$output = ssh $DockerSshAlias "printf '%s' '$encoded' | base64 -d | docker exec -i rap_postgres psql -U rap_user -d remote_access_platform -t -A -v ON_ERROR_STOP=1"
|
|
return (($output | Select-Object -Last 1) -as [string]).Trim()
|
|
}
|
|
|
|
function Invoke-Api([string]$Method, [string]$Path, [object]$Body = $null) {
|
|
$uri = "$BackendApiBase/$Path"
|
|
if ($null -eq $Body) {
|
|
return Invoke-RestMethod -Method $Method -Uri $uri
|
|
}
|
|
return Invoke-RestMethod -Method $Method -Uri $uri -ContentType "application/json" -Body ($Body | ConvertTo-Json -Depth 16)
|
|
}
|
|
|
|
function Restart-SmokeWorker {
|
|
$cmd = "docker rm -f $WorkerContainerName 2>/dev/null || true; docker run -d --name $WorkerContainerName --network host -e RDP_WORKER_ID=rdp-worker-1 -e RDP_WORKER_REDIS_HOST=127.0.0.1 -e RDP_WORKER_REDIS_PORT=6379 -e RDP_WORKER_TRANSFER_ROOT=/tmp/rap-rdp-worker-transfers -e RDP_WORKER_CAPABILITIES=adaptive-quality,dirty-rects,clipboard,file-transfer $WorkerImage"
|
|
$output = ssh $DockerSshAlias $cmd
|
|
Write-SmokeLog "worker restart output=$($output -join ' ')"
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
|
|
function New-GatewayWebSocket([string]$AttachToken) {
|
|
$ws = [Net.WebSockets.ClientWebSocket]::new()
|
|
$uri = [Uri]::new("${BackendWsBase}?attach_token=$([Uri]::EscapeDataString($AttachToken))")
|
|
$null = $ws.ConnectAsync($uri, [Threading.CancellationToken]::None).GetAwaiter().GetResult()
|
|
return $ws
|
|
}
|
|
|
|
function Receive-Envelope([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$TimeoutMilliseconds = 30000) {
|
|
$buffer = [byte[]]::new(1024 * 1024)
|
|
$segment = [ArraySegment[byte]]::new($buffer)
|
|
$stream = [IO.MemoryStream]::new()
|
|
while ($true) {
|
|
$cts = [Threading.CancellationTokenSource]::new($TimeoutMilliseconds)
|
|
try {
|
|
$result = $WebSocket.ReceiveAsync($segment, $cts.Token).GetAwaiter().GetResult()
|
|
}
|
|
finally {
|
|
$cts.Dispose()
|
|
}
|
|
if ($result.MessageType -eq [Net.WebSockets.WebSocketMessageType]::Close) {
|
|
throw "websocket closed"
|
|
}
|
|
$stream.Write($buffer, 0, $result.Count)
|
|
if ($result.EndOfMessage) {
|
|
break
|
|
}
|
|
}
|
|
return ([Text.Encoding]::UTF8.GetString($stream.ToArray()) | ConvertFrom-Json)
|
|
}
|
|
|
|
function Receive-EnvelopeOfType([Net.WebSockets.ClientWebSocket]$WebSocket, [string[]]$Types, [int]$TimeoutSeconds = 30) {
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
$envelope = Receive-Envelope -WebSocket $WebSocket -TimeoutMilliseconds 10000
|
|
if ($Types -contains [string]$envelope.type) {
|
|
return $envelope
|
|
}
|
|
}
|
|
throw "timed out waiting for envelope type: $($Types -join ',')"
|
|
}
|
|
|
|
function Send-Envelope([Net.WebSockets.ClientWebSocket]$WebSocket, [object]$Envelope) {
|
|
$json = $Envelope | ConvertTo-Json -Depth 16 -Compress
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
|
|
$null = $WebSocket.SendAsync([ArraySegment[byte]]::new($bytes), [Net.WebSockets.WebSocketMessageType]::Text, $true, [Threading.CancellationToken]::None).GetAwaiter().GetResult()
|
|
}
|
|
|
|
function Wait-SessionActive([Net.WebSockets.ClientWebSocket]$WebSocket) {
|
|
$deadline = (Get-Date).AddSeconds(90)
|
|
while ((Get-Date) -lt $deadline) {
|
|
$envelope = Receive-Envelope -WebSocket $WebSocket -TimeoutMilliseconds 30000
|
|
Write-SmokeLog "ws event type=$($envelope.type) state=$($envelope.payload.state)"
|
|
if ($envelope.type -eq "session.state" -and $envelope.payload.state -eq "active") {
|
|
return
|
|
}
|
|
}
|
|
throw "session did not become active"
|
|
}
|
|
|
|
function Send-Key([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$ScanCode, [bool]$Extended = $false) {
|
|
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_down"; scan_code = $ScanCode; is_extended = $Extended } }
|
|
Start-Sleep -Milliseconds 60
|
|
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_up"; scan_code = $ScanCode; is_extended = $Extended } }
|
|
}
|
|
|
|
function Send-Chord([Net.WebSockets.ClientWebSocket]$WebSocket, [int]$ModifierScanCode, [bool]$ModifierExtended, [int]$KeyScanCode) {
|
|
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_down"; scan_code = $ModifierScanCode; is_extended = $ModifierExtended } }
|
|
Start-Sleep -Milliseconds 80
|
|
Send-Key $WebSocket $KeyScanCode $false
|
|
Start-Sleep -Milliseconds 80
|
|
Send-Envelope $WebSocket @{ type = "input"; payload = @{ kind = "keyboard"; action = "key_up"; scan_code = $ModifierScanCode; is_extended = $ModifierExtended } }
|
|
}
|
|
|
|
function Upload-TextFile([Net.WebSockets.ClientWebSocket]$WebSocket) {
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes("Stage 5.1.1 RAP_Transfers visible file`r`nрусский текст`r`n")
|
|
$transferId = [guid]::NewGuid().ToString("D")
|
|
Send-Envelope $WebSocket @{
|
|
type = "file_upload.start"
|
|
payload = @{
|
|
direction = "client_to_server"
|
|
transfer_id = $transferId
|
|
file_name = "stage5-upload-text.txt"
|
|
file_size = [int64]$bytes.Length
|
|
total_chunks = 1
|
|
content_hash = ""
|
|
}
|
|
}
|
|
$started = Receive-EnvelopeOfType $WebSocket @("file_upload.progress", "file_upload.blocked") 20
|
|
if ($started.type -ne "file_upload.progress") {
|
|
throw "upload start was blocked"
|
|
}
|
|
Send-Envelope $WebSocket @{
|
|
type = "file_upload.chunk"
|
|
payload = @{
|
|
direction = "client_to_server"
|
|
transfer_id = $transferId
|
|
chunk_index = 0
|
|
offset = 0
|
|
chunk_size = $bytes.Length
|
|
chunk_bytes = [Convert]::ToBase64String($bytes)
|
|
}
|
|
}
|
|
$done = Receive-EnvelopeOfType $WebSocket @("file_upload.progress", "file_upload.blocked") 20
|
|
Write-SmokeLog "upload result type=$($done.type) status=$($done.payload.status)"
|
|
if ($done.type -ne "file_upload.progress" -or $done.payload.status -ne "completed") {
|
|
throw "upload did not complete"
|
|
}
|
|
}
|
|
|
|
function Write-BgraFrameToBmp([string]$SessionId, [string]$Path) {
|
|
$state = $null
|
|
$data = $null
|
|
$width = 0
|
|
$height = 0
|
|
$imageSize = 0
|
|
$deadline = (Get-Date).AddSeconds(20)
|
|
do {
|
|
$json = ssh $DockerSshAlias "docker exec rap_redis redis-cli --raw GET live:session:$SessionId"
|
|
if ($json) {
|
|
$state = ($json -join "`n") | ConvertFrom-Json
|
|
$width = [int]$state.render_width
|
|
$height = [int]$state.render_height
|
|
$stride = $width * 4
|
|
$imageSize = $stride * $height
|
|
if ($width -gt 0 -and $height -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$state.render_frame_data)) {
|
|
$data = [Convert]::FromBase64String([string]$state.render_frame_data)
|
|
if ($data.Length -ge $imageSize) {
|
|
break
|
|
}
|
|
Write-SmokeLog "waiting for full framebuffer in live state bytes=$($data.Length) expected=$imageSize sequence=$($state.render_frame_sequence)"
|
|
}
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
} while ((Get-Date) -lt $deadline)
|
|
|
|
if ($null -eq $data -or $data.Length -lt $imageSize) {
|
|
$actualBytes = if ($null -eq $data) { 0 } else { $data.Length }
|
|
throw "frame data too small or unavailable bytes=$actualBytes expected=$imageSize"
|
|
}
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Path) | Out-Null
|
|
$fs = [IO.File]::Open($Path, [IO.FileMode]::Create, [IO.FileAccess]::Write)
|
|
try {
|
|
$writer = [IO.BinaryWriter]::new($fs)
|
|
$fileSize = 14 + 40 + $imageSize
|
|
$writer.Write([byte][char]'B')
|
|
$writer.Write([byte][char]'M')
|
|
$writer.Write([uint32]$fileSize)
|
|
$writer.Write([uint16]0)
|
|
$writer.Write([uint16]0)
|
|
$writer.Write([uint32]54)
|
|
$writer.Write([uint32]40)
|
|
$writer.Write([int32]$width)
|
|
$writer.Write([int32](-$height))
|
|
$writer.Write([uint16]1)
|
|
$writer.Write([uint16]32)
|
|
$writer.Write([uint32]0)
|
|
$writer.Write([uint32]$imageSize)
|
|
$writer.Write([int32]2835)
|
|
$writer.Write([int32]2835)
|
|
$writer.Write([uint32]0)
|
|
$writer.Write([uint32]0)
|
|
$writer.Write($data, 0, $imageSize)
|
|
}
|
|
finally {
|
|
$fs.Dispose()
|
|
}
|
|
Write-SmokeLog "wrote frame $Path"
|
|
}
|
|
|
|
Restart-SmokeWorker
|
|
|
|
$login = Invoke-Api Post "auth/login" @{
|
|
email = $Email
|
|
password = $Password
|
|
trust_device = $true
|
|
device_fingerprint = "stage5-drive-visibility"
|
|
device_label = "stage5-drive-visibility"
|
|
}
|
|
$userId = [string]$login.user.id
|
|
if ([string]::IsNullOrWhiteSpace($userId)) { $userId = [string]$login.user.ID }
|
|
$deviceId = [string]$login.device.id
|
|
if ([string]::IsNullOrWhiteSpace($deviceId)) { $deviceId = [string]$login.device.ID }
|
|
|
|
$escapedResourceName = $ResourceName.Replace("'", "''")
|
|
$resourceId = Invoke-RemoteSqlScalar "SELECT id::text FROM resources WHERE name = '$escapedResourceName' LIMIT 1;"
|
|
$null = Invoke-RemoteSqlScalar "UPDATE resource_policies SET file_transfer_mode = 'client_to_server', file_transfer_enabled = TRUE, clipboard_mode = 'client_to_server', clipboard_enabled = TRUE, updated_at = NOW() WHERE resource_id = '$resourceId'; SELECT 'ok';"
|
|
|
|
$session = Invoke-Api Post "sessions" @{
|
|
resource_id = $resourceId
|
|
user_id = $userId
|
|
device_id = $deviceId
|
|
}
|
|
$sessionId = [string]$session.session.id
|
|
if ([string]::IsNullOrWhiteSpace($sessionId)) { $sessionId = [string]$session.session.ID }
|
|
$attachToken = [string]$session.attach_token.token
|
|
Write-SmokeLog "visibility session=$sessionId"
|
|
$ws = New-GatewayWebSocket $attachToken
|
|
try {
|
|
Wait-SessionActive $ws
|
|
Upload-TextFile $ws
|
|
Start-Sleep -Seconds 2
|
|
Send-Envelope $ws @{ type = "clipboard"; payload = @{ direction = "client_to_server"; text = "\\tsclient\RAP_Transfers\stage5-upload-text.txt"; sequence_id = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds(); origin = "visibility-smoke"; content_hash = "" } }
|
|
Start-Sleep -Seconds 1
|
|
Send-Chord $ws 91 $true 19
|
|
Start-Sleep -Seconds 1
|
|
Send-Chord $ws 29 $false 47
|
|
Start-Sleep -Milliseconds 300
|
|
Send-Key $ws 28 $false
|
|
Start-Sleep -Seconds 5
|
|
Write-BgraFrameToBmp $sessionId $OutputFrame
|
|
}
|
|
finally {
|
|
$ws.Dispose()
|
|
try {
|
|
Invoke-Api Post "sessions/$sessionId/terminate" @{ user_id = $userId; reason = "stage5_drive_visibility_cleanup" } | Out-Null
|
|
}
|
|
catch {
|
|
Write-SmokeLog "terminate skipped: $($_.Exception.Message)"
|
|
}
|
|
}
|