PowerRemoteDesktop_Viewer.psm1
<#-------------------------------------------------------------------------------
Power Remote Desktop In loving memory of my father. Thanks for all you've done. you will remain in my heart forever. .Developer Jean-Pierre LESUEUR (@DarkCoderSc) https://www.twitter.com/darkcodersc https://github.com/DarkCoderSc www.phrozen.io jplesueur@phrozen.io PHROZEN .License Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ .Disclaimer We are doing our best to prepare the content of this app. However, PHROZEN SASU and / or Jean-Pierre LESUEUR cannot warranty the expressions and suggestions of the contents, as well as its accuracy. In addition, to the extent permitted by the law, PHROZEN SASU and / or Jean-Pierre LESUEUR shall not be responsible for any losses and/or damages due to the usage of the information on our app. By using our app, you hereby consent to our disclaimer and agree to its terms. Any links contained in our app may lead to external sites are provided for convenience only. Any information or statements that appeared in these sites or app are not sponsored, endorsed, or otherwise approved by PHROZEN SASU and / or Jean-Pierre LESUEUR. For these external sites, PHROZEN SASU and / or Jean-Pierre LESUEUR cannot be held liable for the availability of, or the content located on or through it. Plus, any losses or damages occurred from using these contents or the internet generally. -------------------------------------------------------------------------------#> Add-Type -Assembly System.Windows.Forms Add-Type @" using System; using System.Runtime.InteropServices; public static class User32 { [DllImport("User32.dll")] public static extern bool SetProcessDPIAware(); } "@ $global:PowerRemoteDesktopVersion = "3.1.0" $global:HostSyncHash = [HashTable]::Synchronized(@{ host = $host ClipboardText = (Get-Clipboard -Raw) }) $global:EphemeralTrustedServers = @() $global:LocalStoragePath = "HKCU:\SOFTWARE\PowerRemoteDesktop_Viewer" $global:LocalStoragePath_TrustedServers = -join($global:LocalStoragePath, "\TrustedServers") enum ClipboardMode { Disabled = 1 Receive = 2 Send = 3 Both = 4 } enum ProtocolCommand { Success = 1 Fail = 2 RequestSession = 3 AttachToSession = 4 BadRequest = 5 ResourceFound = 6 ResourceNotFound = 7 } enum WorkerKind { Desktop = 1 Events = 2 } enum BlockSize { Size32 = 32 Size64 = 64 Size96 = 96 Size128 = 128 Size256 = 256 Size512 = 512 } enum PacketSize { Size1024 = 1024 Size2048 = 2048 Size4096 = 4096 Size8192 = 8192 Size9216 = 9216 Size12288 = 12288 Size16384 = 16384 } function Write-Banner { <# .SYNOPSIS Output cool information about current PowerShell module to terminal. #> Write-Host "" Write-Host "Power Remote Desktop - Version " -NoNewLine Write-Host $global:PowerRemoteDesktopVersion -ForegroundColor Cyan Write-Host "Jean-Pierre LESUEUR (" -NoNewLine Write-Host "@DarkCoderSc" -NoNewLine -ForegroundColor Green Write-Host ") " -NoNewLine Write-Host "#" -NoNewLine -ForegroundColor Blue Write-Host "#" -NoNewLine -ForegroundColor White Write-Host "#" -ForegroundColor Red Write-Host "https://" -NoNewLine -ForegroundColor Green Write-Host "www.github.com/darkcodersc" Write-Host "https://" -NoNewLine -ForegroundColor Green Write-Host "www.phrozen.io" Write-Host "" Write-Host "License: Apache License (Version 2.0, January 2004)" Write-Host "https://" -NoNewLine -ForegroundColor Green Write-Host "www.apache.org/licenses/" Write-Host "" } function Get-BooleanAnswer { <# .SYNOPSIS As user to make a boolean choice. Return True if Y and False if N. #> while ($true) { $choice = Read-Host "[Y] Yes [N] No (Default is ""N"")" if (-not $choice) { $choice = "N" } switch ($choice) { "Y" { return $true } "N" { return $false } default { Write-Host "Invalid Answer, available options are ""Y , N""" -ForegroundColor Red } } } } function New-RegistryStorage { <# .SYNOPSIS Create required registry keys for storing persistent data between viewer sessions. .DESCRIPTION Users doesn't share this storage. If you really wish to, replace HKCU by HKLM (Requires Admin Privilege) #> try { if (-not (Test-Path -Path $global:LocalStoragePath)) { Write-Verbose "Create local storage root at ""${global:LocalStoragePath}""..." New-Item -Path $global:LocalStoragePath } if (-not (Test-Path -Path $global:LocalStoragePath_TrustedServers)) { Write-Verbose "Create local storage child: ""${global:LocalStoragePath}""..." New-Item -Path $global:LocalStoragePath_TrustedServers } } catch { Write-Verbose "Could not write server fingerprint to local storage with error: ""$($_)""" } } function Write-ServerFingerprintToLocalStorage { <# .SYNOPSIS Write a trusted server certificate fingerprint to our local storage. .PARAMETER Fingerprint Type: String Default: None Description: Fingerprint to store in local storage. #> param ( [Parameter(Mandatory=$True)] [string] $Fingerprint ) New-RegistryStorage # Value is stored as a JSON Object to be easily upgraded and extended in future. $value = New-Object -TypeName PSCustomObject -Property @{ FirstSeen = (Get-Date).ToString() } New-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -PropertyType "String" -Value ($value | ConvertTo-Json -Compress) -ErrorAction Ignore } function Remove-TrustedServer { <# .SYNOPSIS Remove trusted server from local storage. .PARAMETER Fingerprint Type: String Default: None Description: Fingerprint to remove from local storage. #> param ( [Parameter(Mandatory=$True)] [string] $Fingerprint ) if (-not (Test-ServerFingerprintFromLocalStorage -Fingerprint $Fingerprint)) { throw "Could not find fingerprint on trusted server list." } Write-Host "You are about to permanently delete trusted server -> """ -NoNewline Write-Host $Fingerprint -NoNewLine -ForegroundColor Green Write-Host """" Write-Host "Are you sure ?" if (Get-BooleanAnswer) { Remove-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint Write-Host "Server successfully untrusted." } } function Get-TrustedServers { <# .SYNOPSIS Return a list of trusted servers fingerprints from local storage. #> $list = @() Get-Item -Path $global:LocalStoragePath_TrustedServers -ErrorAction Ignore | Select-Object -ExpandProperty Property | ForEach-Object { try { $list += New-Object -TypeName PSCustomObject -Property @{ Fingerprint = $_ Detail = (Get-ItemPropertyValue -Path $global:LocalStoragePath_TrustedServers -Name $_) | ConvertFrom-Json } } catch { } } return $list } function Clear-TrustedServers { <# .SYNOPSIS Remove all trusted servers from local storage. #> $trustedServers = Get-TrustedServers if (@($trustedServers).Length -eq 0) { throw "No trusted servers so far." } Write-Host "You are about to permanently delete $(@(trustedServers).Length) trusted servers." Write-Host "Are you sure ?" if (Get-BooleanAnswer) { Remove-Item -Path $global:LocalStoragePath_TrustedServers -Force -Verbose Write-Host "Servers successfully untrusted." } } function Test-ServerFingerprintFromLocalStorage { <# .SYNOPSIS Check if a server certificate fingerprint was saved to local storage. .PARAMETER Fingerprint Type: String Default: None Description: Fingerprint to check in local storage. #> param ( [Parameter(Mandatory=$True)] [string] $Fingerprint ) return (Get-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -ErrorAction Ignore) } function Get-SHA512FromString { <# .SYNOPSIS Return the SHA512 value from string. .PARAMETER String Type: String Default : None Description: A String to hash. .EXAMPLE Get-SHA512FromString -String "Hello, World" #> param ( [Parameter(Mandatory=$True)] [string] $String ) $buffer = [IO.MemoryStream]::new([byte[]][char[]]$String) return (Get-FileHash -InputStream $buffer -Algorithm SHA512).Hash } function Resolve-AuthenticationChallenge { <# .SYNOPSIS Algorithm to solve the server challenge during password authentication. .DESCRIPTION Server needs to resolve the challenge and keep the solution in memory before sending the candidate to remote peer. .PARAMETER Password Type: SecureString Default: None Description: Secure String object containing the password for resolving challenge. .PARAMETER Candidate Type: String Default: None Description: Random string used to solve the challenge. This string is public and is set across network by server. Each time a new connection is requested to server, a new candidate is generated. .EXAMPLE Resolve-AuthenticationChallenge -Password "s3cr3t!" -Candidate "rKcjdh154@]=Ldc" #> param ( [Parameter(Mandatory=$True)] [SecureString] $SecurePassword, [Parameter(Mandatory=$True)] [string] $Candidate ) $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) try { $solution = -join($Candidate, ":", [Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)) for ([int] $i = 0; $i -le 1000; $i++) { $solution = Get-SHA512FromString -String $solution } return $solution } finally { [Runtime.InteropServices.Marshal]::FreeBSTR($BSTR) } } class ClientIO { [string] $RemoteAddress [int] $RemotePort [bool] $UseTLSv1_3 [System.Net.Sockets.TcpClient] $Client = $null [System.Net.Security.SslStream] $SSLStream = $null [System.IO.StreamWriter] $Writer = $null [System.IO.StreamReader] $Reader = $null [System.IO.BinaryReader] $BinaryReader = $null ClientIO( [string] $RemoteAddress = "127.0.0.1", [int] $RemotePort = 2801, [bool] $UseTLSv1_3 = $false ) { $this.RemoteAddress = $RemoteAddress $this.RemotePort = $RemotePort $this.UseTLSv1_3 = $UseTLSv1_3 } [void]Connect() { <# .SYNOPSIS Open a new connection to remote server. Create required streams and open a new secure connection with peer. #> Write-Verbose "Connect: ""$($this.RemoteAddress):$($this.RemotePort)...""" $this.Client = New-Object System.Net.Sockets.TcpClient($this.RemoteAddress, $this.RemotePort) Write-Verbose "Connected." if ($this.UseTLSv1_3) { $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 } else { $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 } Write-Verbose "Establish an encrypted tunnel using: ${TLSVersion}..." $this.SSLStream = New-object System.Net.Security.SslStream( $this.Client.GetStream(), $false, { param( $Sendr, $Certificate, $Chain, $Policy ) if ( (Test-ServerFingerprintFromLocalStorage -Fingerprint $Certificate.Thumbprint) -or $global:EphemeralTrustedServers -contains $Certificate.Thumbprint ) { Write-Verbose "Fingerprint already known and trusted: ""$($Certificate.Thumbprint)""" return $true } else { Write-Verbose "@Remote Server Certificate:" Write-Verbose $Certificate Write-Verbose "---" Write-Host "Untrusted Server Certificate Fingerprint: """ -NoNewLine Write-Host $Certificate.Thumbprint -NoNewline -ForegroundColor Green Write-Host """" while ($true) { Write-Host "`r`nDo you want to trust current server ?" $choice = Read-Host "[A] Always [Y] Yes [N] No [?] Help (Default is ""N"")" if (-not $choice) { $choice = "N" } switch ($choice) { "?" { Write-Host "" Write-Host "[" -NoNewLine Write-Host "A" -NoNewLine -ForegroundColor Cyan Write-Host "] Always trust current server (Persistent between PowerShell Instances)" Write-Host "[" -NoNewLine Write-Host "Y" -NoNewLine -ForegroundColor Cyan Write-Host "] Trust current server during current PowerShell Instance lifetime (Temporary)." Write-Host "[" -NoNewLine Write-Host "N" -NoNewLine -ForegroundColor Cyan Write-Host "] Don't trust current server. Connection is aborted (Recommeneded if you don't recognize server fingerprint)." Write-Host "[" -NoNewLine Write-Host "?" -NoNewLine -ForegroundColor Cyan Write-Host "] Current help output." Write-Host "" } "A" { Write-ServerFingerprintToLocalStorage -Fingerprint $Certificate.Thumbprint return $true } "Y" { $global:EphemeralTrustedServers += $Certificate.Thumbprint return $true } "N" { return $false } default { Write-Host "Invalid Answer, available options are ""A , Y , N , H""" -ForegroundColor Red } } } } } ) $this.SSLStream.AuthenticateAsClient( "PowerRemoteDesktop", $null, $TLSVersion, $null ) if (-not $this.SSLStream.IsEncrypted) { throw "Could not establish a secure communication channel with remote server." } $this.SSLStream.WriteTimeout = 5000 $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) $this.Writer.AutoFlush = $true $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) $this.BinaryReader = New-Object System.IO.BinaryReader($this.SSLStream) Write-Verbose "Encrypted tunnel opened and ready for use." } [void]Authentify([SecureString] $SecurePassword) { <# .SYNOPSIS Handle authentication process with remote peer. .PARAMETER Password Type: SecureString Default: None Description: Secure String object containing the password. .EXAMPLE .Authentify((ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)) #> Write-Verbose "Authentify with remote server (Challenged-Based Authentication)..." $candidate = $this.Reader.ReadLine() $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -SecurePassword $SecurePassword Write-Verbose "@Challenge:" Write-Verbose "Candidate: ""${candidate}""" Write-Verbose "Solution: ""${challengeSolution}""" Write-Verbose "---" $this.Writer.WriteLine($challengeSolution) $result = $this.Reader.ReadLine() if ($result -eq [ProtocolCommand]::Success) { Write-Verbose "Solution accepted. Authentication success." } else { throw "Solution declined. Authentication failed." } } [string] RemoteAddress() { return $this.Client.Client.RemoteEndPoint.Address } [int] RemotePort() { return $this.Client.Client.RemoteEndPoint.Port } [string] LocalAddress() { return $this.Client.Client.LocalEndPoint.Address } [int] LocalPort() { return $this.Client.Client.LocalEndPoint.Port } [string] ReadLine([int] $Timeout) { <# .SYNOPSIS Read string message from remote peer with timeout support. .PARAMETER Timeout Type: Integer Description: Maximum period of time to wait for incomming data. #> $defautTimeout = $this.SSLStream.ReadTimeout try { $this.SSLStream.ReadTimeout = $Timeout return $this.Reader.ReadLine() } finally { $this.SSLStream.ReadTimeout = $defautTimeout } } [string] ReadLine() { <# .SYNOPSIS Shortcut to Reader ReadLine method. No timeout support. #> return $this.Reader.ReadLine() } [void] WriteJson([PSCustomObject] $Object) { <# .SYNOPSIS Transform a PowerShell Object as a JSON Representation then send to remote peer. .PARAMETER Object Type: PSCustomObject Description: Object to be serialized in JSON. #> $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) } [void] WriteLine([string] $Value) { $this.Writer.WriteLine($Value) } [PSCustomObject] ReadJson([int] $Timeout) { <# .SYNOPSIS Read json string from remote peer and attempt to deserialize as a PowerShell Object. .PARAMETER Timeout Type: Integer Description: Maximum period of time to wait for incomming data. #> return ($this.ReadLine($Timeout) | ConvertFrom-Json) } [PSCustomObject] ReadJson() { <# .SYNOPSIS Alternative to ReadJson without timeout support. #> return ($this.ReadLine() | ConvertFrom-Json) } [void]Close() { <# .SYNOPSIS Release Streams and Connections. #> if ($this.Writer) { $this.Writer.Close() } if ($this.Reader) { $this.Reader.Close() } if ($this.BinaryReader) { $this.BinaryReader.Close() } if ($this.SSLStream) { $this.SSLStream.Close() } if ($this.Client) { $this.Client.Close() } } } class ViewerConfiguration { [bool] $RequireResize = $false [int] $VirtualDesktopWidth = 0 [int] $VirtualDesktopHeight = 0 [int] $ScreenX_Delta = 0 [int] $ScreenY_Delta = 0 [float] $ScreenX_Ratio = 1 [float] $ScreenY_Ratio = 1 } class ViewerSession { [PSCustomObject] $ServerInformation = $null [ViewerConfiguration] $ViewerConfiguration = $null [string] $ServerAddress = "127.0.0.1" [string] $ServerPort = 2801 [SecureString] $SecurePassword = $null [bool] $UseTLSv1_3 = $false [int] $ImageCompressionQuality = 100 [int] $ResizeRatio = 0 [PacketSize] $PacketSize = [PacketSize]::Size9216 [BlockSize] $BlockSize = [BlockSize]::Size64 [bool] $FastResize = $false [ClientIO] $ClientDesktop = $null [ClientIO] $ClientEvents = $null ViewerSession( [string] $ServerAddress, [int] $ServerPort, [SecureString] $SecurePassword ) { # Or: System.Management.Automation.Runspaces.MaxPort (High(Word)) if ($ServerPort -lt 0 -and $ServerPort -gt 65535) { throw "Invalid TCP Port (0-65535)" } $this.ServerAddress = $ServerAddress $this.ServerPort = $ServerPort $this.SecurePassword = $SecurePassword } [void] OpenSession() { <# .SYNOPSIS Request a new session with remote server. #> Write-Verbose "Request new session with remote server: ""$($this.ServerAddress):$($this.ServerPort)""..." if ($this.ServerInformation) { throw "A session already exists." } Write-Verbose "Establish first contact with remote server..." $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) try { $client.Connect() $client.Authentify($this.SecurePassword) Write-Verbose "Request session..." $client.WriteLine(([ProtocolCommand]::RequestSession)) $this.ServerInformation = $client.ReadJson() Write-Verbose "@ServerInformation:" Write-Verbose $this.ServerInformation Write-Verbose "---" if ( (-not ($this.ServerInformation.PSobject.Properties.name -contains "SessionId")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "Version")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "ViewOnly")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "MachineName")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "Username")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "WindowsVersion")) -or (-not ($this.ServerInformation.PSobject.Properties.name -contains "Screens")) ) { throw "Invalid server information object." } Write-Verbose "Server informations acknowledged, prepare and send our expectation..." if ($this.ServerInformation.Version -ne $global:PowerRemoteDesktopVersion) { throw "Server and Viewer version mismatch.`r`n` Local: ""${global:PowerRemoteDesktopVersion}""`r`n` Remote: ""$($this.ServerInformation.Version)""`r`n` You cannot use two different version between Viewer and Server." } if ($this.ServerInformation.ViewOnly) { Write-Host "You are accessing a read-only desktop." -ForegroundColor Cyan } # Define which screen we want to capture $selectedScreen = $null if ($this.ServerInformation.Screens.Length -gt 1) { Write-Verbose "Remote server have $($this.ServerInformation.Screens.Length) screens." Write-Host "Remote server have " -NoNewLine Write-Host $($this.ServerInformation.Screens.Length) -NoNewLine -ForegroundColor Green Write-Host " different screens:`r`n" foreach ($screen in $this.ServerInformation.Screens) { Write-Host $screen.Id -NoNewLine -ForegroundColor Cyan Write-Host " - $($screen.Name)" -NoNewLine if ($screen.Primary) { Write-Host " (" -NoNewLine Write-Host "Primary" -NoNewLine -ForegroundColor Cyan Write-Host ")" -NoNewLine } Write-Host "" } while ($true) { $choice = Read-Host "`r`nPlease choose which screen index to capture (Default: Primary)" if (-not $choice) { # Select-Object -First 1 should also grab the Primary Screen (Since it is ordered). $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Primary -eq $true } } else { if (-not $choice -is [int]) { Write-Host "You must enter a valid index (integer), starting at 1." continue } $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Id -eq $choice } if (-not $selectedScreen) { Write-Host "Invalid choice, please choose an existing screen index." -ForegroundColor Red } } if ($selectedScreen) { break } } } else { $selectedScreen = $this.ServerInformation.Screens | Select-Object -First 1 } # Define our Virtual Desktop Form constraints $localScreenWidth = Get-LocalScreenWidth $localScreenHeight = (Get-LocalScreenHeight) - (Get-WindowCaptionHeight) $this.ViewerConfiguration = [ViewerConfiguration]::New() # If remote screen is bigger than local screen, we will resize remote screen to fit 90% of local screen. # Supports screen orientation (Horizontal / Vertical) if ($localScreenWidth -le $selectedScreen.Width -or $localScreenHeight -le $selectedScreen.Height) { $adjustRatio = 90 $adjustVertically = $localScreenWidth -gt $localScreenHeight if ($adjustVertically) { $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $adjustRatio) / 100) $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopWidth * 100) / $selectedScreen.Width) $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($selectedScreen.Height * $remoteResizedRatio) / 100) } else { $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($localScreenHeight * $adjustRatio) / 100) $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopHeight * 100) / $selectedScreen.Height) $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($selectedScreen.Width * $remoteResizedRatio) / 100) } } else { $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height } # If remote desktop resize is forced, we apply defined ratio to current configuration if ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 99) { $this.ViewerConfiguration.VirtualDesktopWidth = ($selectedScreen.Width * $this.ResizeRatio) / 100 $this.ViewerConfiguration.VirtualDesktopHeight = ($selectedScreen.Height * $this.ResizeRatio) / 100 } $this.ViewerConfiguration.RequireResize = $this.ViewerConfiguration.VirtualDesktopWidth -ne $selectedScreen.Width -or $this.ViewerConfiguration.VirtualDesktopHeight -ne $selectedScreen.Height $this.ViewerConfiguration.ScreenX_Delta = $selectedScreen.X $this.ViewerConfiguration.ScreenY_Delta = $selectedScreen.Y $viewerExpectation = New-Object PSCustomObject -Property @{ ScreenName = $selectedScreen.Name ImageCompressionQuality = $this.ImageCompressionQuality PacketSize = $this.PacketSize BlockSize = $this.BlockSize FastResize = $this.FastResize } if ($this.ViewerConfiguration.RequireResize) { $viewerExpectation | Add-Member -MemberType NoteProperty -Name "ExpectDesktopWidth" -Value $this.ViewerConfiguration.VirtualDesktopWidth $viewerExpectation | Add-Member -MemberType NoteProperty -Name "ExpectDesktopHeight" -Value $this.ViewerConfiguration.VirtualDesktopHeight $this.ViewerConfiguration.ScreenX_Ratio = $selectedScreen.Width / $this.ViewerConfiguration.VirtualDesktopWidth $this.ViewerConfiguration.ScreenY_Ratio = $selectedScreen.Height / $this.ViewerConfiguration.VirtualDesktopHeight } Write-Verbose "@ViewerExpectation:" Write-Verbose $viewerExpectation Write-Verbose "---" $client.WriteJson($viewerExpectation) if ($client.ReadLine(5 * 1000) -cne [ProtocolCommand]::Success) { throw "Remote server did not respond to our expectation in time." } } catch { $this.CloseSession() throw "Could not open a new session with error: ""$($_)""" } finally { if ($client) { $client.Close() } } } [ClientIO] ConnectWorker([WorkerKind] $WorkerKind) { Write-Verbose "Connect new worker: ""$WorkerKind""..." $this.CheckSession() $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) try { $client.Connect() $client.Authentify($this.SecurePassword) $client.WriteLine(([ProtocolCommand]::AttachToSession)) Write-Verbose "Attach worker to remote session ""$($this.ServerInformation.SessionId)""" $client.WriteLine($this.ServerInformation.SessionId) switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) { ([ProtocolCommand]::ResourceFound) { Write-Verbose "Worker successfully attached to session, define which kind of worker we are..." $client.WriteLine($WorkerKind) Write-Verbose "Worker ready." break } ([ProtocolCommand]::ResourceNotFound) { throw "Server could not locate session." } default { throw "Unexpected answer from remote server for session attach." } } return $client } catch { if ($client) { $client.Close() } throw "Could not connect worker with error: $($_)" } } [void] ConnectDesktopWorker() { Write-Verbose "Create new desktop streaming worker..." $this.ClientDesktop = $this.ConnectWorker([WorkerKind]::Desktop) } [void] ConnectEventsWorker() { Write-Verbose "Create new event event (in/out) worker..." $this.ClientEvents = $this.ConnectWorker([WorkerKind]::Events) } [bool] HasSession() { return $this.ServerInformation -and $this.ViewerConfiguration } [void] CheckSession() { if (-not $this.HasSession) { throw "Session is missing." } } [void] CloseSession() { <# .SYNOPSIS Close an existing session with remote server. Terminate active connections and reset session informations. #> Write-Verbose "Close existing session..." if ($this.ClientDesktop) { $this.ClientDesktop.Close() } if ($this.ClientEvents) { $this.ClientEvents.Close() } $this.ClientDesktop = $null $this.ClientEvents = $null $this.ServerInformation = $null $this.ViewerConfiguration = $null Write-Verbose "Session closed." } } $global:VirtualDesktopUpdaterScriptBlock = { try { $packetSize = 9216 # 9KiB # SizeOf(DWORD) * 3 (SizeOf(Desktop) + SizeOf(Left) + SizeOf(Top)) $struct = New-Object -TypeName byte[] -ArgumentList (([Runtime.InteropServices.Marshal]::SizeOf([System.Type][UInt32])) * 3) $stream = New-Object System.IO.MemoryStream $scene = $null $sceneGraphics = $null $destPoint = [System.Drawing.Point]::New(0, 0) while ($true) { try { $null = $Param.Client.SSLStream.Read($struct, 0, $struct.Length) $totalBufferSize = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x0) $destPoint.X = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x4) $destPoint.Y = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x8) $stream.SetLength($totalBufferSize) $stream.Position = 0 do { $bufferSize = $stream.Length - $stream.Position if ($bufferSize -gt $packetSize) { $bufferSize = $packetSize } $null = $stream.Write($Param.Client.BinaryReader.ReadBytes($bufferSize), 0, $bufferSize) } until ($stream.Position -eq $stream.Length) if ($stream.Length -eq 0) { continue } if (-not $scene) { # First Iteration $scene = [System.Drawing.Image]::FromStream($stream) $sceneGraphics = [System.Drawing.Graphics]::FromImage($scene) $sceneGraphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = $scene } else { # Next Iterations $sceneGraphics.DrawImage( [System.Drawing.Image]::FromStream($stream), $destPoint ) $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Invalidate() } } catch { break } } } finally { if ($scene) { $scene.Dispose() } if ($sceneGraphics) { $sceneGraphics.Dispose() } if ($stream) { $stream.Close() } $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.Close() } } $global:IngressEventScriptBlock = { enum CursorType { IDC_APPSTARTING IDC_ARROW IDC_CROSS IDC_HAND IDC_HELP IDC_IBEAM IDC_ICON IDC_NO IDC_SIZE IDC_SIZEALL IDC_SIZENESW IDC_SIZENS IDC_SIZENWSE IDC_SIZEWE IDC_UPARROW IDC_WAIT } enum InputEvent { KeepAlive = 0x1 MouseCursorUpdated = 0x2 ClipboardUpdated = 0x3 } enum ClipboardMode { Disabled = 1 Receive = 2 Send = 3 Both = 4 } while ($true) { try { $jsonEvent = $Param.Client.Reader.ReadLine() } catch { break } try { $aEvent = $jsonEvent | ConvertFrom-Json } catch { continue } if (-not ($aEvent.PSobject.Properties.name -match "Id")) { continue } switch ([InputEvent] $aEvent.Id) { # Remote Global Mouse Cursor State Changed (Icon) ([InputEvent]::MouseCursorUpdated) { if (-not ($aEvent.PSobject.Properties.name -match "Cursor")) { continue } $cursor = [System.Windows.Forms.Cursors]::Arrow switch ([CursorType] $aEvent.Cursor) { ([CursorType]::IDC_APPSTARTING) { $cursor = [System.Windows.Forms.Cursors]::AppStarting } ([CursorType]::IDC_CROSS) { $cursor = [System.Windows.Forms.Cursors]::Cross } ([CursorType]::IDC_HAND) { $cursor = [System.Windows.Forms.Cursors]::Hand } ([CursorType]::IDC_HELP) { $cursor = [System.Windows.Forms.Cursors]::Help } ([CursorType]::IDC_IBEAM) { $cursor = [System.Windows.Forms.Cursors]::IBeam } ([CursorType]::IDC_NO) { $cursor = [System.Windows.Forms.Cursors]::No } ([CursorType]::IDC_SIZENESW) { $cursor = [System.Windows.Forms.Cursors]::SizeNESW } ([CursorType]::IDC_SIZENS) { $cursor = [System.Windows.Forms.Cursors]::SizeNS } ([CursorType]::IDC_SIZENWSE) { $cursor = [System.Windows.Forms.Cursors]::SizeNWSE } ([CursorType]::IDC_SIZEWE) { $cursor = [System.Windows.Forms.Cursors]::SizeWE } ([CursorType]::IDC_UPARROW) { $cursor = [System.Windows.Forms.Cursors]::UpArrow } ([CursorType]::IDC_WAIT) { $cursor = [System.Windows.Forms.Cursors]::WaitCursor } {( $_ -eq ([CursorType]::IDC_SIZE) -or $_ -eq ([CursorType]::IDC_SIZEALL) )} { $cursor = [System.Windows.Forms.Cursors]::SizeAll } } try { $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Cursor = $cursor } catch {} break } ([InputEvent]::ClipboardUpdated) { if ($Param.Clipboard -eq ([ClipboardMode]::Disabled) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) { continue } if (-not ($aEvent.PSobject.Properties.name -match "Text")) { continue } $HostSyncHash.ClipboardText = $aEvent.Text Set-Clipboard -Value $aEvent.Text } } } } $global:EgressEventScriptBlock = { enum OutputEvent { # 0x1 0x2 0x3 are at another place (GUI Thread) KeepAlive = 0x4 ClipboardUpdated = 0x5 } enum ClipboardMode { Disabled = 1 Receive = 2 Send = 3 Both = 4 } function Send-Event { <# .SYNOPSIS Send an event to remote peer. .PARAMETER AEvent Define what kind of event to send. .PARAMETER Data An optional object containing additional information about the event. #> param ( [Parameter(Mandatory=$True)] [OutputEvent] $AEvent, [PSCustomObject] $Data = $null ) try { if (-not $Data) { $Data = New-Object -TypeName PSCustomObject -Property @{ Id = $AEvent } } else { $Data | Add-Member -MemberType NoteProperty -Name "Id" -Value $AEvent } $Param.OutputEventSyncHash.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) return $true } catch { return $false } } $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() while ($true) { # Events that occurs every seconds needs to be placed bellow. # If no event has occured during this second we send a Keep-Alive signal to # remote peer and detect a potential socket disconnection. if ($stopWatch.ElapsedMilliseconds -ge 1000) { try { $eventTriggered = $false if ($Param.Clipboard -eq ([ClipboardMode]::Both) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) { # IDEA: Check for existing clipboard change event or implement a custom clipboard # change detector using "WM_CLIPBOARDUPDATE" for example (WITHOUT INLINE C#) # It is not very important but it would avoid calling "Get-Clipboard" every seconds. $currentClipboard = (Get-Clipboard -Raw) if ($currentClipboard -and $currentClipboard -cne $HostSyncHash.ClipboardText) { $data = New-Object -TypeName PSCustomObject -Property @{ Text = $currentClipboard } if (-not (Send-Event -AEvent ([OutputEvent]::ClipboardUpdated) -Data $data)) { break } $HostSyncHash.ClipboardText = $currentClipboard $eventTriggered = $true } } # Send a Keep-Alive if during this second iteration nothing happened. if (-not $eventTriggered) { if (-not (Send-Event -AEvent ([OutputEvent]::KeepAlive))) { break } } } finally { $stopWatch.Restart() } } } } function Get-WindowCaptionHeight { $form = New-Object System.Windows.Forms.Form try { $screenRect = $form.RectangleToScreen($form.ClientRectangle) return $screenRect.Top - $virtualDesktopSyncHash.VirtualDesktop.Form.Top } finally { if ($form) { $form.Dispose() } } } function Get-LocalScreenWidth { return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Width } function Get-LocalScreenHeight { return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Height } function New-VirtualDesktopForm { <# .SYNOPSIS Create new WinForms Components to handle Virtual Desktop. .DESCRIPTION This function first create a new Windows Form then create a new child component (PaintBox) to display remote desktop frames. It returns a PowerShell object containing both Form and PaintBox. .PARAMETER Width Type: Integer Default: 1200 Description: The pre-defined width of new form .PARAMETER Height Type: Integer Default: 800 Description: The pre-defined height of new form .PARAMETER Caption Type: String Default: PowerRemoteDesktop Viewer Description: The pre-defined caption of new form. .EXAMPLE New-VirtualDesktopForm -Caption "New Desktop Form" -Width 1200 -Height 800 #> param ( [int] $Width = 1200, [int] $Height = 800, [string] $Caption = "PowerRemoteDesktop Viewer" ) $form = New-Object System.Windows.Forms.Form $form.Width = $Width $form.Height = $Height $form.BackColor = [System.Drawing.Color]::Black $form.Text = $Caption $form.KeyPreview = $true # Necessary to capture keystrokes. $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle $form.MaximizeBox = $false $pictureBox = New-Object System.Windows.Forms.PictureBox $pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill $form.Controls.Add($pictureBox) return New-Object PSCustomObject -Property @{ Form = $form Picture = $pictureBox } } function New-RunSpace { <# .SYNOPSIS Create a new PowerShell Runspace. .DESCRIPTION Notice: the $host variable is used for debugging purpose to write on caller PowerShell Terminal. .PARAMETER ScriptBlock Type: ScriptBlock Default: None Description: Instructions to execute in new runspace. .PARAMETER Param Type: PSCustomObject Default: None Description: Object to attach in runspace context. .EXAMPLE New-RunSpace -Client $newClient -ScriptBlock { Start-Sleep -Seconds 10 } #> param( [Parameter(Mandatory=$True)] [ScriptBlock] $ScriptBlock, [PSCustomObject] $Param = $null ) $runspace = [RunspaceFactory]::CreateRunspace() $runspace.ThreadOptions = "ReuseThread" $runspace.ApartmentState = "STA" $runspace.Open() if ($Param) { $runspace.SessionStateProxy.SetVariable("Param", $Param) } $runspace.SessionStateProxy.SetVariable("HostSyncHash", $global:HostSyncHash) $powershell = [PowerShell]::Create().AddScript($ScriptBlock) $powershell.Runspace = $runspace $asyncResult = $powershell.BeginInvoke() return New-Object PSCustomObject -Property @{ Runspace = $runspace PowerShell = $powershell AsyncResult = $asyncResult } } function Invoke-RemoteDesktopViewer { <# .SYNOPSIS Open a new remote desktop session with a remote server. .DESCRIPTION Notice: Prefer using SecurePassword over plain-text password even if a plain-text password is getting converted to SecureString anyway. .PARAMETER ServerAddress Type: String Default: 127.0.0.1 Description: Remote server host/address. .PARAMETER ServerPort Type: Integer Default: 2801 (0 - 65535) Description: Remote server port. .PARAMETER SecurePassword Type: SecureString Default: None Description: SecureString object containing password used to authenticate with remote server (Recommended) .PARAMETER Password Type: String Default: None Description: Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) .PARAMETER UseTLSv1_3 Type: Switch Default: False Description: If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) .PARAMETER DisableVerbosity Type: Boolean Default: False Description: If present, program wont show verbosity messages. .PARAMETER Clipboard Type: Enum Default: Both Description: Define clipboard synchronization mode (Both, Disabled, Send, Receive) see bellow for more detail. * Disabled -> Clipboard synchronization is disabled in both side * Receive -> Only incomming clipboard is allowed * Send -> Only outgoing clipboard is allowed * Both -> Clipboard synchronization is allowed on both side .PARAMETER ImageCompressionQuality Type: Integer (0 - 100) Default: 75 Description: JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. .PARAMETER Resize Type: Switch Default: None Description: If present, remote desktop will get resized accordingly with ResizeRatio option. .PARAMETER ResizeRatio Type: Integer (30 - 99) Default: None Description: Used with Resize option, define the resize ratio in percentage. .PARAMETER AlwaysOnTop Type: Switch Default: False Description: If present, virtual desktop form will be above all other window's. .PARAMETER BlockSize Type: Enum Values: Size32, Size64, Size96, Size128, Size256, Size512 Default: Size64 Description: (Advanced) Define the screen grid block size. Choose the block size accordingly to remote screen size / computer constrainsts (CPU / Network) Size1024 -> 1024 Bytes (1KiB) Size2048 -> 2048 Bytes (2KiB) Size4096 -> 4096 Bytes (4KiB) Size8192 -> 8192 Bytes (8KiB) Size9216 -> 9216 Bytes (9KiB) Size12288 -> 12288 Bytes (12KiB) Size16384 -> 16384 Bytes (16KiB) .PARAMETER PacketSize Type: Enum Values: Size1024, Size2048, Size4096, Size8192, Size9216, Size12288, Size16384 Default: Size9216 Description: (Advanced) Define the network packet size for streams. Choose the packet size accordingly to your network constrainsts. Size32 -> 32x32 Size64 -> 64x64 Size96 -> 96x96 Size128 -> 128x128 Size256 -> 256x256 Size512 -> 512x512 .PARAMETER FastResize Type: Switch Default: None Description: Control the quality of remote desktop resize (smoothing) if applicable. If you lack of network speed, this option is recommended. .EXAMPLE Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -Password "s3cr3t!" Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort "2801" -Password "Just4TestingLocally!" #> param ( [string] $ServerAddress = "127.0.0.1", [ValidateRange(0, 65535)] [int] $ServerPort = 2801, [switch] $UseTLSv1_3, [SecureString] $SecurePassword, [String] $Password, [switch] $DisableVerbosity, [ClipboardMode] $Clipboard = [ClipboardMode]::Both, [ValidateRange(0, 100)] [int] $ImageCompressionQuality = 75, [switch] $Resize, [ValidateRange(30, 99)] [int] $ResizeRatio = 90, [switch] $AlwaysOnTop, [PacketSize] $PacketSize = [PacketSize]::Size9216, [BlockSize] $BlockSize = [BlockSize]::Size64, [switch] $FastResize = $false ) [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() $oldErrorActionPreference = $ErrorActionPreference $oldVerbosePreference = $VerbosePreference try { $ErrorActionPreference = "stop" if (-not $DisableVerbosity) { $VerbosePreference = "continue" } else { $VerbosePreference = "SilentlyContinue" } Write-Banner $null = [User32]::SetProcessDPIAware() Write-Verbose "Server address: ""${ServerAddress}:${ServerPort}""" if (-not $SecurePassword -and -not $Password) { throw "You must specify either a SecurePassword or Password parameter used during server authentication." } if ($Password -and -not $SecurePassword) { $SecurePassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) Remove-Variable -Name "Password" -ErrorAction SilentlyContinue } $session = [ViewerSession]::New( $ServerAddress, $ServerPort, $SecurePassword ) try { $session.UseTLSv1_3 = $UseTLSv1_3 $session.ImageCompressionQuality = $ImageCompressionQuality $session.PacketSize = $PacketSize $session.BlockSize = $BlockSize $session.FastResize = $FastResize if ($Resize) { $session.ResizeRatio = $ResizeRatio } Write-Host "Start new remote desktop session..." $session.OpenSession() $session.ConnectDesktopWorker() $session.ConnectEventsWorker() Write-Host "Session successfully established, start streaming..." Write-Verbose "Create WinForms Environment..." $virtualDesktopSyncHash = [HashTable]::Synchronized(@{ VirtualDesktop = New-VirtualDesktopForm }) $virtualDesktopSyncHash.VirtualDesktop.Form.Text = [string]::Format( "Power Remote Desktop v{0}: {1}/{2} - {3}", $global:PowerRemoteDesktopVersion, $session.ServerInformation.Username, $session.ServerInformation.MachineName, $session.ServerInformation.WindowsVersion ) # Size Virtual Desktop Form Window $virtualDesktopSyncHash.VirtualDesktop.Form.ClientSize = [System.Drawing.Size]::New( $session.ViewerConfiguration.VirtualDesktopWidth, $session.ViewerConfiguration.VirtualDesktopHeight ) # Create a thread-safe hashtable to send events to remote server. $outputEventSyncHash = [HashTable]::Synchronized(@{ Writer = $session.ClientEvents.Writer }) # WinForms Events (If enabled, I recommend to disable control when testing on local machine to avoid funny things) if (-not $session.ServerInformation.ViewOnly) { enum OutputEvent { Keyboard = 0x1 MouseClickMove = 0x2 MouseWheel = 0x3 } enum MouseState { Up = 0x1 Down = 0x2 Move = 0x3 } function New-MouseEvent { <# .SYNOPSIS Generate a new mouse event object to be sent to server. This event is used to simulate mouse move and clicks. .PARAMETER X Type: Integer Default: None Description: The position of mouse in horizontal axis. .PARAMETER Y Type: Integer Default: None Description: The position of mouse in vertical axis. .PARAMETER Type Type: Enum Default: None Description: The type of mouse event (Example: Move, Click) .PARAMETER Button Type: String Default: None Description: The pressed button on mouse (Example: Left, Right, Middle) .EXAMPLE New-MouseEvent -X 10 -Y 35 -Type "Up" -Button "Left" New-MouseEvent -X 10 -Y 35 -Type "Down" -Button "Left" New-MouseEvent -X 100 -Y 325 -Type "Move" #> param ( [Parameter(Mandatory=$true)] [int] $X, [Parameter(Mandatory=$true)] [int] $Y, [Parameter(Mandatory=$true)] [MouseState] $Type, [string] $Button = "None" ) return New-Object PSCustomObject -Property @{ Id = [OutputEvent]::MouseClickMove X = $X Y = $Y Button = $Button Type = $Type } } function New-KeyboardEvent { <# .SYNOPSIS Generate a new keyboard event object to be sent to server. This event is used to simulate keyboard strokes. .PARAMETER Keys Type: String Default: None Description: Plain text keys to be simulated on remote computer. .EXAMPLE New-KeyboardEvent -Keys "Hello, World" New-KeyboardEvent -Keys "t" #> param ( [Parameter(Mandatory=$true)] [string] $Keys ) return New-Object PSCustomObject -Property @{ Id = [OutputEvent]::Keyboard Keys = $Keys } } function Send-VirtualMouse { <# .SYNOPSIS Transform the virtual mouse (the one in Virtual Desktop Form) coordinates to real remote desktop screen coordinates (especially when incomming desktop frames are resized) When event is generated, it is immediately sent to remote server. .PARAMETER X Type: Integer Default: None Description: The position of virtual mouse in horizontal axis. .PARAMETER Y Type: Integer Default: None Description: The position of virtual mouse in vertical axis. .PARAMETER Type Type: Integer Default: None Description: The type of mouse event (Example: Move, Click) .PARAMETER Button Type: Integer Default: None Description: The pressed button on mouse (Example: Left, Right, Middle) .EXAMPLE Send-VirtualMouse -X 10 -Y 20 -Type "Move" #> param ( [Parameter(Mandatory=$True)] [int] $X, [Parameter(Mandatory=$True)] [int] $Y, [Parameter(Mandatory=$True)] [MouseState] $Type, [string] $Button = "" ) if ($session.ViewerConfiguration.RequireResize) { $X *= $session.ViewerConfiguration.ScreenX_Ratio $Y *= $session.ViewerConfiguration.ScreenY_Ratio } $X += $session.ViewerConfiguration.ScreenX_Delta $Y += $session.ViewerConfiguration.ScreenY_Delta $aEvent = (New-MouseEvent -X $X -Y $Y -Button $Button -Type $Type) try { $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) } catch {} } function Send-VirtualKeyboard { <# .SYNOPSIS Send to remote server key strokes to simulate. .PARAMETER KeyChain Type: String Default: None Description: A string representing character(s) to simulate remotely. .EXAMPLE Send-VirtualKeyboard -KeyChain "Hello, World" Send-VirtualKeyboard -KeyChain "{LEFT}" #> param ( [Parameter(Mandatory=$True)] [string] $KeyChain ) $aEvent = (New-KeyboardEvent -Keys $KeyChain) try { $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) } catch {} } $virtualDesktopSyncHash.VirtualDesktop.Form.Add_KeyPress( { if ($_.KeyChar) { switch -CaseSensitive ([string]$_.KeyChar) { "{" { $result = "{{}" } "}" { $result = "{}}" } "+" { $result = "{+}" } "^" { $result = "{^}" } "%" { $result = "{%}" } "~" { $result = "{~}" } "(" { $result = "{(}" } ")" { $result = "{)}" } "[" { $result = "{[}" } "]" { $result = "{]}" } default { $result = $_ } } Send-VirtualKeyboard -KeyChain $result } } ) $virtualDesktopSyncHash.VirtualDesktop.Form.Add_Shown( { # Center Virtual Desktop Form $virtualDesktopSyncHash.VirtualDesktop.Form.Location = [System.Drawing.Point]::New( ((Get-LocalScreenWidth) - $virtualDesktopSyncHash.VirtualDesktop.Form.Width) / 2, ((Get-LocalScreenHeight) - $virtualDesktopSyncHash.VirtualDesktop.Form.Height) / 2 ) $virtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $AlwaysOnTop } ) $virtualDesktopSyncHash.VirtualDesktop.Form.Add_KeyDown( { $result = "" switch ($_.KeyValue) { # F Keys 112 { $result = "{F1}" } 113 { $result = "{F2}" } 114 { $result = "{F3}" } 115 { $result = "{F4}" } 116 { $result = "{F5}" } 117 { $result = "{F6}" } 118 { $result = "{F7}" } 119 { $result = "{F8}" } 120 { $result = "{F9}" } 121 { $result = "{F10}" } 122 { $result = "{F11}" } 123 { $result = "{F12}" } 124 { $result = "{F13}" } 125 { $result = "{F14}" } 126 { $result = "{F15}" } 127 { $result = "{F16}" } # Arrows 37 { $result = "{LEFT}" } 38 { $result = "{UP}" } 39 { $result = "{RIGHT}" } 40 { $result = "{DOWN}" } # Misc 92 { $result = "{WIN}" } 27 { $result = "{ESC}"} 33 { $result = "{PGUP}" } 34 { $result = "{PGDW}" } 36 { $result = "{HOME}" } 46 { $result = "{DELETE}" } 35 { $result = "{END}" } # Add other keys bellow } if ($result) { Send-VirtualKeyboard -KeyChain $result } } ) $virtualDesktopSyncHash.VirtualDesktop.Picture.Add_MouseDown( { Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Down) } ) $virtualDesktopSyncHash.VirtualDesktop.Picture.Add_MouseUp( { Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Up) } ) $virtualDesktopSyncHash.VirtualDesktop.Picture.Add_MouseMove( { Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Move) } ) $virtualDesktopSyncHash.VirtualDesktop.Picture.Add_MouseWheel( { $aEvent = New-Object PSCustomObject -Property @{ Id = [OutputEvent]::MouseWheel Delta = $_.Delta } try { $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) } catch {} } ) } Write-Verbose "Create runspace for desktop streaming..." $param = New-Object -TypeName PSCustomObject -Property @{ Client = $session.ClientDesktop VirtualDesktopSyncHash = $virtualDesktopSyncHash } $newRunspace = (New-RunSpace -ScriptBlock $global:VirtualDesktopUpdaterScriptBlock -Param $param) $runspaces.Add($newRunspace) Write-Verbose "Create runspace for incoming events..." $param = New-Object -TypeName PSCustomObject -Property @{ Client = $session.ClientEvents VirtualDesktopSyncHash = $virtualDesktopSyncHash Clipboard = $Clipboard } $newRunspace = (New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param) $runspaces.Add($newRunspace) Write-Verbose "Create runspace for outgoing events..." $param = New-Object -TypeName PSCustomObject -Property @{ OutputEventSyncHash = $outputEventSyncHash Clipboard = $Clipboard } $newRunspace = (New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param) $runspaces.Add($newRunspace) Write-Verbose "Done. Showing Virtual Desktop Form." $null = $virtualDesktopSyncHash.VirtualDesktop.Form.ShowDialog() } finally { Write-Verbose "Free environement." if ($session) { $session.CloseSession() $session = $null } Write-Verbose "Free runspaces..." foreach ($runspace in $runspaces) { $null = $runspace.PowerShell.EndInvoke($runspace.AsyncResult) $runspace.PowerShell.Runspace.Dispose() $runspace.PowerShell.Dispose() } $runspaces.Clear() if ($virtualDesktopSyncHash.VirtualDesktop) { $virtualDesktopSyncHash.VirtualDesktop.Form.Dispose() } Write-Host "Remote desktop session has ended." } } finally { $ErrorActionPreference = $oldErrorActionPreference $VerbosePreference = $oldVerbosePreference } } try { Export-ModuleMember -Function Remove-TrustedServer Export-ModuleMember -Function Clear-TrustedServers Export-ModuleMember -Function Get-TrustedServers Export-ModuleMember -Function Invoke-RemoteDesktopViewer } catch {} |