Private/Invoke-GuacProtocolHandshake.ps1
|
function Invoke-GuacProtocolHandshake { <# .SYNOPSIS Performs the Guacamole protocol handshake over a WebSocket connection. .DESCRIPTION Given an already-connected WebSocket and the tunnel URL, performs the handshake sequence: 1. select {connectionId} 2. args {argNames...} 3. size {width,height,dpi} 4. audio {mimeTypes...} 5. video {mimeTypes...} 6. image {mimeTypes...} 7. timezone {tz} 8. connect {argValues...} Then reads the 'ready' instruction to get the session ID. Reference: guacamole-common/src/main/java/org/apache/guacamole/protocol/ ConfiguredGuacamoleSocket.java (handshake sequence), TunnelRequest.java (tunnel URL parameters) .EXAMPLE Invoke-GuacProtocolHandshake -WebSocket $ws -Server $srv -Token $tok -ConnectionId $id -Arguments $args #> [CmdletBinding()] [OutputType([N2C_GuacAdmin_GuacActiveSession])] param ( [Parameter(Mandatory = $true)] [System.Net.WebSockets.ClientWebSocket] $WebSocket, [Parameter(Mandatory = $true)] [N2C_GuacAdmin_GuacSession] $Session, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $ConnectionId = [string]::Empty, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $GroupId = [string]::Empty, [Parameter(Mandatory = $false)] [ValidateRange(64, 7680)] [int] $Width = 1024, [Parameter(Mandatory = $false)] [ValidateRange(64, 7680)] [int] $Height = 768, [Parameter(Mandatory = $false)] [ValidateRange(75, 300)] [int] $Dpi = 96, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string] $Timezone = [string]::Empty, [Parameter(Mandatory = $false)] [AllowEmptyString()] [string[]] $AudioMimeTypes = @(), [Parameter(Mandatory = $false)] [AllowEmptyString()] [string[]] $VideoMimeTypes = @(), [Parameter(Mandatory = $false)] [AllowEmptyString()] [string[]] $ImageMimeTypes = @(), [Parameter(Mandatory = $false)] [AllowEmptyString()] [hashtable] $Arguments = @{}, [Parameter(Mandatory = $false)] [ValidateRange(5, 300)] [int] $HandshakeTimeoutSec = 30 ) # Helper: send instruction and wait for response # Note: In the real Guacamole protocol, the handshake is request/response, # but for simplicity we send all instructions and then wait for 'ready'. # 1. select - tell server which connection to use if (-not [string]::IsNullOrWhiteSpace($ConnectionId)) { $selectInstr = $script:GuacInstructionType::new('select', @($ConnectionId)) $selectStr = ConvertTo-GuacInstructionString -Instruction $selectInstr $selectBytes = [System.Text.Encoding]::UTF8.GetBytes($selectStr) $selectSegment = New-Object System.ArraySegment[byte] -ArgumentList $selectBytes [void]$WebSocket.SendAsync($selectSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } elseif (-not [string]::IsNullOrWhiteSpace($GroupId)) { $selectInstr = $script:GuacInstructionType::new('select', @($GroupId)) $selectStr = ConvertTo-GuacInstructionString -Instruction $selectInstr $selectBytes = [System.Text.Encoding]::UTF8.GetBytes($selectStr) $selectSegment = New-Object System.ArraySegment[byte] -ArgumentList $selectBytes [void]$WebSocket.SendAsync($selectSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # 2. args - list of argument names the client understands $argNames = @($Arguments.Keys) $argsInstr = $script:GuacInstructionType::new('args', $argNames) $argsStr = ConvertTo-GuacInstructionString -Instruction $argsInstr $argsBytes = [System.Text.Encoding]::UTF8.GetBytes($argsStr) $argsSegment = New-Object System.ArraySegment[byte] -ArgumentList $argsBytes [void]$WebSocket.SendAsync($argsSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() # 3. size - display resolution $sizeInstr = $script:GuacInstructionType::new('size', @("$Width", "$Height", "$Dpi")) $sizeStr = ConvertTo-GuacInstructionString -Instruction $sizeInstr $sizeBytes = [System.Text.Encoding]::UTF8.GetBytes($sizeStr) $sizeSegment = New-Object System.ArraySegment[byte] -ArgumentList $sizeBytes [void]$WebSocket.SendAsync($sizeSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() # 4. audio - supported audio MIME types if ($AudioMimeTypes.Count -gt 0) { $audioInstr = $script:GuacInstructionType::new('audio', $AudioMimeTypes) $audioStr = ConvertTo-GuacInstructionString -Instruction $audioInstr $audioBytes = [System.Text.Encoding]::UTF8.GetBytes($audioStr) $audioSegment = New-Object System.ArraySegment[byte] -ArgumentList $audioBytes [void]$WebSocket.SendAsync($audioSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # 5. video - supported video MIME types if ($VideoMimeTypes.Count -gt 0) { $videoInstr = $script:GuacInstructionType::new('video', $VideoMimeTypes) $videoStr = ConvertTo-GuacInstructionString -Instruction $videoInstr $videoBytes = [System.Text.Encoding]::UTF8.GetBytes($videoStr) $videoSegment = New-Object System.ArraySegment[byte] -ArgumentList $videoBytes [void]$WebSocket.SendAsync($videoSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # 6. image - supported image MIME types if ($ImageMimeTypes.Count -gt 0) { $imageInstr = $script:GuacInstructionType::new('image', $ImageMimeTypes) $imageStr = ConvertTo-GuacInstructionString -Instruction $imageInstr $imageBytes = [System.Text.Encoding]::UTF8.GetBytes($imageStr) $imageSegment = New-Object System.ArraySegment[byte] -ArgumentList $imageBytes [void]$WebSocket.SendAsync($imageSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # 7. timezone if (-not [string]::IsNullOrWhiteSpace($Timezone)) { $tzInstr = $script:GuacInstructionType::new('timezone', @($Timezone)) $tzStr = ConvertTo-GuacInstructionString -Instruction $tzInstr $tzBytes = [System.Text.Encoding]::UTF8.GetBytes($tzStr) $tzSegment = New-Object System.ArraySegment[byte] -ArgumentList $tzBytes [void]$WebSocket.SendAsync($tzSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # 8. connect - provide argument values if ($Arguments.Count -gt 0) { $argValues = @() foreach ($name in $Arguments.Keys) { $argValues += $Arguments[$name] } $connectInstr = $script:GuacInstructionType::new('connect', $argValues) $connectStr = ConvertTo-GuacInstructionString -Instruction $connectInstr $connectBytes = [System.Text.Encoding]::UTF8.GetBytes($connectStr) $connectSegment = New-Object System.ArraySegment[byte] -ArgumentList $connectBytes [void]$WebSocket.SendAsync($connectSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } else { # Still send a connect instruction (empty) to trigger the connection $connectInstr = $script:GuacInstructionType::new('connect') $connectStr = ConvertTo-GuacInstructionString -Instruction $connectInstr $connectBytes = [System.Text.Encoding]::UTF8.GetBytes($connectStr) $connectSegment = New-Object System.ArraySegment[byte] -ArgumentList $connectBytes [void]$WebSocket.SendAsync($connectSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult() } # Now wait for the 'ready' instruction $buffer = New-Object byte[] 4096 $bufferSegment = New-Object System.ArraySegment[byte] -ArgumentList $buffer # Set up cancellation for timeout $cts = New-Object System.Threading.CancellationTokenSource $cts.CancelAfter([TimeSpan]::FromSeconds($HandshakeTimeoutSec)) $token = $cts.Token # Receive loop: collect instructions until we see 'ready' $sessionBuffer = [System.Text.StringBuilder]::new() while (-not $token.IsCancellationRequested) { $result = $WebSocket.ReceiveAsync($bufferSegment, $token).GetAwaiter().GetResult() if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { throw ($script:GuacRestExceptionType::new('WebSocket closed during handshake.')) } # Convert received bytes to string $receivedText = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count) [void]$sessionBuffer.Append($receivedText) # Parse complete instructions (terminated by ';') $bufferStr = $sessionBuffer.ToString() while ($true) { $semiIdx = $bufferStr.IndexOf(';') if ($semiIdx -lt 0) { break } $completeInstr = $bufferStr.Substring(0, $semiIdx + 1) $bufferStr = $bufferStr.Substring($semiIdx + 1) $sessionBuffer.Length = 0 [void]$sessionBuffer.Append($bufferStr) # Parse the instruction $instr = ConvertFrom-GuacInstructionString -Raw $completeInstr if ($instr.Opcode -eq 'error') { $errorCode = 'UNKNOWN' $errorMsg = 'Unknown error' if ($instr.Arguments.Length -ge 2) { $errorCode = $instr.Arguments[0] $errorMsg = $instr.Arguments[1] } throw ($script:GuacRestExceptionType::new("Guacamole handshake error [$errorCode]: $errorMsg")) } elseif ($instr.Opcode -eq 'ready') { if ($instr.Arguments.Length -ge 1) { $sessionId = $instr.Arguments[0] Write-Verbose ("N2C.GuacAdmin: handshake complete, session ID: {0}" -f $sessionId) $activeSession = $script:GuacActiveSessionType::new() $activeSession.Session = $Session $activeSession.Id = $sessionId $activeSession.ConnectionId = $ConnectionId $activeSession.Protocol = 'guacamole' $activeSession.WebSocket = $WebSocket return $activeSession } } elseif ($instr.Opcode -eq 'disconnect') { $msg = 'Disconnected during handshake' if ($instr.Arguments.Length -ge 1) { $msg = $instr.Arguments[0] } throw ($script:GuacRestExceptionType::new($msg)) } # Other instructions (like 'name') are informational; continue waiting for 'ready' } } throw ($script:GuacRestExceptionType::new("Guacamole handshake timed out after ${HandshakeTimeoutSec}s.")) } |