From c3eb83ad6482fe75dd7e4af328f76e2a852a1108 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Tue, 25 Jan 2022 15:06:11 +0100 Subject: [PATCH 01/17] Concurrency, Fixes, New Protocol --- Assets/iconv2.png | Bin 0 -> 162533 bytes .../PowerRemoteDesktop_Server.psm1 | 2471 ++++++++++------- .../PowerRemoteDesktop_Viewer.psm1 | 778 +++--- README.md | 13 +- TestViewer.ps1 | 2 +- 5 files changed, 1871 insertions(+), 1393 deletions(-) create mode 100644 Assets/iconv2.png diff --git a/Assets/iconv2.png b/Assets/iconv2.png new file mode 100644 index 0000000000000000000000000000000000000000..5323f9511410c9e97af1b37cd51655d377857410 GIT binary patch literal 162533 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

a5*hanHLTPkL=ajyuDDfQEQD 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 KHLTQki~2v@f$m`d literal 0 HcmV?d00001 diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 37b357c..6c39eee 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -52,12 +52,11 @@ Add-Type -Assembly System.Windows.Forms Add-Type -Assembly System.Drawing Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();[DllImport("User32.dll")] public static extern int LoadCursorA(int hInstance, int lpCursorName);[DllImport("User32.dll")] public static extern bool GetCursorInfo(IntPtr pci);' -Name User32 -Namespace W; -$global:PowerRemoteDesktopVersion = "1.0.6" +$global:PowerRemoteDesktopVersion = "2.0.0" $global:HostSyncHash = [HashTable]::Synchronized(@{ host = $host - ClipboardText = (Get-Clipboard -Raw) - RunningSession = $false + ClipboardText = (Get-Clipboard -Raw) }) enum ClipboardMode { @@ -128,8 +127,7 @@ function New-RandomPassword .DESCRIPTION Generate new password candidates until one candidate match complexity rules. - Generally only one iteration is enough but in some rare case it could be one or two more. - TODO: Better algorithm to avoid loop ? + Generally only one iteration is enough but in some rare case it could be one or two more. #> do { @@ -490,6 +488,39 @@ function Get-SHA512FromString return (Get-FileHash -InputStream $buffer -Algorithm SHA512).Hash } +function Get-ScreenList() +{ + <# + .SYNOPSIS + Return an array of screen objects. + + .DESCRIPTION + A screen refer to physical or virtual screen (monitor). + + #> + $result = @() + + $screens = ([System.Windows.Forms.Screen]::AllScreens | Sort-Object -Property Primary -Descending) + + $i = 0 + foreach ($screen in $screens) + { + $i++ + + $result += New-Object -TypeName PSCustomObject -Property @{ + Id = $i + Name = $screen.DeviceName + Primary = $screen.Primary + Width = $screen.Bounds.Width + Height = $screen.Bounds.Height + X = $screen.Bounds.X + Y = $screen.Bounds.Y + } + } + + return $result +} + function Resolve-AuthenticationChallenge { <# @@ -528,1174 +559,1620 @@ function Resolve-AuthenticationChallenge return $solution } -function Get-LocalMachineInformation -{ - <# - .SYNOPSIS - Generate an object containing few useful information about current machine. - - .DESCRIPTION - Most important part is the target screen information. Without this information, remote viewer - will not be able to correctly draw / adjust desktop image and simulate mouse events. - - This function is expected to be progressively updated with new required session information. - #> - - $screens = @() +$global:DesktopStreamScriptBlock = { - $i = 0 - foreach ($screen in ([System.Windows.Forms.Screen]::AllScreens | Sort-Object -Property Primary -Descending)) + function Invoke-SmoothResize { - $i++ - - $screens += New-Object -TypeName PSCustomObject -Property @{ - Id = $i - Name = $screen.DeviceName - Primary = $screen.Primary - Width = $screen.Bounds.Width - Height = $screen.Bounds.Height - X = $screen.Bounds.X - Y = $screen.Bounds.Y - } - } - - return New-Object PSCustomObject -Property @{ - MachineName = [Environment]::MachineName - Username = [Environment]::UserName - WindowsVersion = [Environment]::OSVersion.VersionString - Screens = ($screens) - } -} - -class ServerSession { - [string] $Id = "" - [string] $TiedAddress = "" - [string] $Screen = "" - - ServerSession([string] $RemoteAddress) { <# .SYNOPSIS - Create a new session. - - .PARAMETER RemoteAddress - IP Address to be tied with session and avoid session impersonation outside of the - network. - #> - - $this.Id = (SHA512FromString -String (-join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}))) - $this.TiedAddress = $RemoteAddress - } - - [bool] CompareWith([string] $Id, [string] $RemoteAddress) { - return ($this.Id -eq $Id) -and ($this.TiedAddress -eq $RemoteAddress) - } -} - -class ClientIO { - [System.Net.Sockets.TcpClient] $Client = $null - [System.IO.StreamWriter] $Writer = $null - [System.IO.StreamReader] $Reader = $null - [System.Net.Security.SslStream] $SSLStream = $null - + Output a resized version of input bitmap. The resize quality is quite fair. + + .PARAMETER OriginalImage + Input bitmap to resize. - ClientIO( - [System.Net.Sockets.TcpClient] $Client, - [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, - [bool] $TLSv1_3 - ) { - <# - .SYNOPSIS - Class constructor. + .PARAMETER NewWidth + Define the width of new bitmap version. - .PARAMETER Client - TcpClient instance returned by listener. + .PARAMETER NewHeight + Define the height of new bitmap version. - .PARAMETER Certificate - X509 Certificate used for SSL/TLS encryption tunnel. + .PARAMETER HighQuality + Activate high quality image resizing with a serious performance cost. - .PARAMETER TLSv1_3 - Define whether or not SSL/TLS v1.3 must be used. + .EXAMPLE + Invoke-SmoothResize -OriginalImage $myImage -NewWidth 1920 -NewHeight 1024 #> + param ( + [Parameter(Mandatory=$true)] + [System.Drawing.Bitmap] $OriginalImage, - if ((-not $Client) -or (-not $Certificate)) - { - throw "ClientIO Class requires both a valid TcpClient and X509Certificate2." - } - - $this.Client = $Client - - Write-Verbose "Create new SSL Stream..." - - $this.SSLStream = New-Object System.Net.Security.SslStream($this.Client.GetStream(), $false) - - if ($TLSv1_3) - { - $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 - } - else { - $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 - } - - Write-Verbose "Authenticate as server using ${TLSVersion}..." - - $this.SSLStream.AuthenticateAsServer( - $Certificate, - $false, - $TLSVersion, - $false - ) - - if (-not $this.SSLStream.IsEncrypted) - { - throw "Could not established an encrypted tunnel with remote peer." - } - - $this.SSLStream.WriteTimeout = 5000 - - Write-Verbose "Open communication channels..." - - $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) - $this.Writer.AutoFlush = $true - - $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) - - Write-Verbose "Connection ready for use." - } - - [bool]Authentify([string] $Password) { - <# - .SYNOPSIS - Handle authentication process with remote peer. + [Parameter(Mandatory=$true)] + [int] $NewWidth, - .PARAMETER Password - Password used to validate challenge and grant access for a new Client. + [Parameter(Mandatory=$true)] + [int] $NewHeight, - .EXAMPLE - .Authentify("s3cr3t!") - #> + [bool] $HighQuality = $false + ) try - { - if (-not $Password) { - throw "During client authentication, a password cannot be blank." - } - - Write-Verbose "New authentication challenge..." - - $candidate = -join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}) - $candidate = Get-SHA512FromString -String $candidate - - $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -Password $Password - - Write-Verbose "@Challenge:" - Write-Verbose "Candidate: ""${candidate}""" - Write-Verbose "Solution: ""${challengeSolution}""" - Write-Verbose "---" - - $this.Writer.WriteLine($candidate) - - Write-Verbose "Candidate sent to client, waiting for answer..." - - $challengeReply = $this.Reader.ReadLine() + { + $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $NewWidth, $NewHeight - Write-Verbose "Replied solution: ""${challengeReply}""" - - # Challenge solution is a Sha512 Hash so comparison doesn't need to be sensitive (-ceq or -cne) - if ($challengeReply -ne $challengeSolution) - { - $this.Writer.WriteLine("KO.") + $resizedImage = [System.Drawing.Graphics]::FromImage($bitmap) - throw "Client challenge solution does not match our solution." + if ($HighQuality) + { + $resizedImage.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality + $resizedImage.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality + $resizedImage.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic + $resizedImage.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality } - else - { - $this.Writer.WriteLine("OK.") - - Write-Verbose "Password Authentication Success" + + $resizedImage.DrawImage($OriginalImage, 0, 0, $bitmap.Width, $bitmap.Height) - return 280121 # True - } + return $bitmap } - catch + finally { - throw "Password Authentication Failed. Reason: `r`n $($_)" + if ($OriginalImage) + { + $OriginalImage.Dispose() + } + + if ($resizedImage) + { + $resizedImage.Dispose() + } } } - [void]Hello([ServerSession] $Session) { + function Get-DesktopImage { <# .SYNOPSIS - This method is called if a sessio + Return a snapshot of primary screen desktop. - .PARAMETER Session - A ServerSession Object Containing Viewer Sesion Information. + .PARAMETER Screen + Define target screen to capture (if multiple monitor exists). + Default is primary screen #> - - Write-Verbose "Session authentication with remote peer..." + param ( + [System.Windows.Forms.Screen] $Screen = $null + ) try - { - $receivedSessionId = $this.Reader.ReadLine() + { + if (-not $Screen) + { + $Screen = [System.Windows.Forms.Screen]::PrimaryScreen + } - Write-Verbose "Peer Session Id: ${receivedSessionId}." + $size = New-Object System.Drawing.Size( + $Screen.Bounds.Size.Width, + $Screen.Bounds.Size.Height + ) - if ($Session.CompareWith($receivedSessionId, $this.RemoteAddress())) - { - $this.Writer.WriteLine("HELLO.") + $location = New-Object System.Drawing.Point( + $Screen.Bounds.Location.X, + $Screen.Bounds.Location.Y + ) - Write-Verbose "Session authentication successful." - } - else - { - $this.Writer.WriteLine("BYE.") + $bitmap = New-Object System.Drawing.Bitmap( + $size.Width, + $size.Height, + [System.Drawing.Imaging.PixelFormat]::Format24bppRgb + ) - throw "Session authentication failed." + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + + $graphics.CopyFromScreen($location, [System.Drawing.Point]::Empty, $size) + + return $bitmap + } + catch + { + if ($bitmap) + { + $bitmap.Dispose() } } - catch + finally { - throw "Session Authentication Failed with extended error: `r`n $($_)" + if ($graphics) + { + $graphics.Dispose() + } } - } + } + + # Initialize locally desktop streaming configuration + # "SafeHash" is a synchronized object, we must avoid accessing this object regularly to improve performance. + $imageQuality = $Param.SafeHash.ViewerConfiguration.ImageCompressionQuality - [ServerSession]Hello([bool] $ViewOnly) { - <# - .SYNOPSIS - Initialize a new session with remote Viewer. - #> + $screen = [System.Windows.Forms.Screen]::AllScreens | Where-Object -FilterScript { + $_.DeviceName -eq $Param.SafeHash.ViewerConfiguration.ScreenName + } - Write-Verbose "Open a new session with remote peer..." + $requireResize = $Param.SafeHash.ViewerConfiguration.ResizeDesktop() + $resizeWidth = $Param.SafeHash.ViewerConfiguration.ExpectDesktopWidth + $resizeHeight = $Param.SafeHash.ViewerConfiguration.ExpectDesktopHeight + try + { + [System.IO.MemoryStream] $oldImageStream = New-Object System.IO.MemoryStream - $session = [ServerSession]::New($this.RemoteAddress()) + $jpegEncoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }; - Write-Verbose "@Session" - Write-Verbose "Id: ""$($session.Id)""" - Write-Verbose "Addr: ""$($session.TiedAddress)""" - Write-Verbose "---" + $encoderParameters = New-Object System.Drawing.Imaging.EncoderParameters(1) + $encoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, $imageQuality) - $sessionInformation = Get-LocalMachineInformation + $packetSize = 9216 # 9KiB + + while ($true) + { + try + { + $desktopImage = Get-DesktopImage -Screen $screen + if ($requireResize) + { + $desktopImage = Invoke-SmoothResize -OriginalImage $desktopImage -NewWidth $resizeWidth -NewHeight $resizeHeight + } - $sessionInformation | Add-Member -MemberType NoteProperty -Name "SessionId" -Value $session.Id - $sessionInformation | Add-Member -MemberType NoteProperty -Name "Version" -Value $global:PowerRemoteDesktopVersion - $sessionInformation | Add-Member -MemberType NoteProperty -Name "ViewOnly" -Value $ViewOnly + $imageStream = New-Object System.IO.MemoryStream - Write-Verbose "Sending Session Information with Local System Information..." + $desktopImage.Save($imageStream, $jpegEncoder, $encoderParameters) - $this.Writer.WriteLine(($sessionInformation | ConvertTo-Json -Compress)) + $sendUpdate = $true - if ($sessionInformation.Screens.Length -gt 1) - { - Write-Verbose "Current system have $($sessionInformation.Screens.Length) Screens. Waiting for Remote Viewer to choose which screen to capture." + # Check both stream size. + $sendUpdate = ($oldImageStream.Length -ne $imageStream.Length) - $screenName = $this.Reader.ReadLine() + # If sizes are equal, compare both Fingerprint to confirm finding. + if (-not $sendUpdate) + { + $imageStream.position = 0 + $oldImageStream.position = 0 - $session.Screen = $screenName - } + $md5_1 = (Get-FileHash -InputStream $imageStream -Algorithm MD5).Hash + $md5_2 = (Get-FileHash -InputStream $oldImageStream -Algorithm MD5).Hash + + $sendUpdate = ($md5_1 -ne $md5_2) + } + + if ($sendUpdate) + { + $imageStream.position = 0 + try + { + $Param.Client.SSLStream.Write([BitConverter]::GetBytes([int32] $imageStream.Length) , 0, 4) # SizeOf(Int32) + + $binaryReader = New-Object System.IO.BinaryReader($imageStream) + do + { + $bufferSize = ($imageStream.Length - $imageStream.Position) + if ($bufferSize -gt $packetSize) + { + $bufferSize = $packetSize + } + + $Param.Client.SSLStream.Write($binaryReader.ReadBytes($bufferSize), 0, $bufferSize) + } until ($imageStream.Position -eq $imageStream.Length) + } + catch + { break } + + # Update Old Image Stream for Comparison + $imageStream.position = 0 - Write-Verbose "Handshake done." + $oldImageStream.SetLength(0) + + $imageStream.CopyTo($oldImageStream) + } + else {} + } + catch + { } + finally + { + if ($desktopImage) + { + $desktopImage.Dispose() + } - return $session + if ($imageStream) + { + $imageStream.Close() + } + } + } } + finally + { + if ($oldImageStream) + { + $oldImageStream.Close() + } + } +} - [string]RemoteAddress() { - return $this.Client.Client.RemoteEndPoint.Address +$global:IngressEventScriptBlock = { + + Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern void mouse_event(int flags, int dx, int dy, int cButtons, int info);[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);' -Name U32 -Namespace W; + + enum MouseFlags { + MOUSEEVENTF_ABSOLUTE = 0x8000 + MOUSEEVENTF_LEFTDOWN = 0x0002 + MOUSEEVENTF_LEFTUP = 0x0004 + MOUSEEVENTF_MIDDLEDOWN = 0x0020 + MOUSEEVENTF_MIDDLEUP = 0x0040 + MOUSEEVENTF_MOVE = 0x0001 + MOUSEEVENTF_RIGHTDOWN = 0x0008 + MOUSEEVENTF_RIGHTUP = 0x0010 + MOUSEEVENTF_WHEEL = 0x0800 + MOUSEEVENTF_XDOWN = 0x0080 + MOUSEEVENTF_XUP = 0x0100 + MOUSEEVENTF_HWHEEL = 0x01000 } - [int]RemotePort() { - return $this.Client.Client.RemoteEndPoint.Port + enum InputEvent { + Keyboard = 0x1 + MouseClickMove = 0x2 + MouseWheel = 0x3 + KeepAlive = 0x4 + ClipboardUpdated = 0x5 } - [string]LocalAddress() { - return $this.Client.Client.LocalEndPoint.Address - } + enum MouseState { + Up = 0x1 + Down = 0x2 + Move = 0x3 + } - [int]LocalPort() { - return $this.Client.Client.LocalEndPoint.Port + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 } - [void]Close() { + class KeyboardSim { <# .SYNOPSIS - Release streams and client. + Class to simulate Keyboard Events using a WScript.Shell + Instance. + #> + [System.__ComObject] $WShell = $null + + KeyboardSim () + <# + .SYNOPSIS + Class constructor #> - - if ($this.Writer) { - $this.Writer.Close() + $this.WShell = New-Object -ComObject WScript.Shell } - if ($this.Reader) + [void] SendInput([string] $String) { - $this.Reader.Close() + <# + .SYNOPSIS + Simulate Keyboard Strokes. It can contain a single char or a complex string. + + .PARAMETER String + Char or String to be simulated as pressed. + + .EXAMPLE + .SendInput("Hello, World") + .SendInput("J") + #> + + # Simulate + $this.WShell.SendKeys($String) + } + } + + $keyboardSim = [KeyboardSim]::New() + + while ($true) + { + try + { + $jsonEvent = $Param.Reader.ReadLine() + } + catch + { + # ($_ | Out-File "c:\temp\debug.txt") + + break + } + + try + { + $aEvent = $jsonEvent | ConvertFrom-Json + } + catch { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Id")) + { continue } + + switch ([InputEvent] $aEvent.Id) + { + # Keyboard Input Simulation + ([InputEvent]::Keyboard) + { + if ($Param.ViewOnly) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Keys")) + { break } + + $keyboardSim.SendInput($aEvent.Keys) + break + } + + # Mouse Move & Click Simulation + ([InputEvent]::MouseClickMove) + { + if ($Param.ViewOnly) + { continue } + + if (-not ($aEvent.PSobject.Properties.name -match "Type")) + { break } + + switch ([MouseState] $aEvent.Type) + { + # Mouse Down/Up + {($_ -eq ([MouseState]::Down)) -or ($_ -eq ([MouseState]::Up))} + { + [W.U32]::SetCursorPos($aEvent.X, $aEvent.Y) + + $down = ($_ -eq ([MouseState]::Down)) + + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTDOWN + if (-not $down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTUP + } + + switch($aEvent.Button) + { + "Right" + { + if ($down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTDOWN + } + else + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTUP + } + + break + } + + "Middle" + { + if ($down) + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEDOWN + } + else + { + $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEUP + } + } + } + [W.U32]::mouse_event($mouseCode, 0, 0, 0, 0); + + break + } + + # Mouse Move + ([MouseState]::Move) + { + if ($Param.ViewOnly) + { continue } + + [W.U32]::SetCursorPos($aEvent.X, $aEvent.Y) + + break + } + } + + break + } + + # Mouse Wheel Simulation + ([InputEvent]::MouseWheel) { + if ($Param.ViewOnly) + { continue } + + [W.U32]::mouse_event([int][MouseFlags]::MOUSEEVENTF_WHEEL, 0, 0, $aEvent.Delta, 0); + + break + } + + # Clipboard Update + ([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 CursorType { + IDC_APPSTARTING = 32650 + IDC_ARROW = 32512 + IDC_CROSS = 32515 + IDC_HAND = 32649 + IDC_HELP = 32651 + IDC_IBEAM = 32513 + IDC_ICON = 32641 + IDC_NO = 32648 + IDC_SIZE = 32640 + IDC_SIZEALL = 32646 + IDC_SIZENESW = 32643 + IDC_SIZENS = 32645 + IDC_SIZENWSE = 32642 + IDC_SIZEWE = 32644 + IDC_UPARROW = 32516 + IDC_WAIT = 32514 + } + + enum OutputEvent { + KeepAlive = 0x1 + MouseCursorUpdated = 0x2 + ClipboardUpdated = 0x3 + } + + enum ClipboardMode { + Disabled = 1 + Receive = 2 + Send = 3 + Both = 4 + } + + function Initialize-Cursors + { + <# + .SYNOPSIS + Initialize different Windows supported mouse cursors. + + .DESCRIPTION + Unfortunately, there is not WinAPI to get current mouse cursor icon state (Ex: as a flag) + but only current mouse cursor icon (via its handle). + + One solution, is to resolve each supported mouse cursor handles (HCURSOR) with corresponding name + in a hashtable and then compare with GetCursorInfo() HCURSOR result. + #> + $cursors = @{} + + foreach ($cursorType in [CursorType].GetEnumValues()) { + $result = [W.User32]::LoadCursorA(0, [int]$cursorType) + + if ($result -gt 0) + { + $cursors[[string] $cursorType] = $result + } + } + + return $cursors + } + + function Get-GlobalMouseCursorIconHandle + { + <# + .SYNOPSIS + Return global mouse cursor handle. + .DESCRIPTION + For this project I really want to avoid using "inline c#" but only pure PowerShell Code. + I'm using a Hackish method to retrieve the global Windows cursor info by playing by hand + with memory to prepare and read CURSORINFO structure. + --- + typedef struct tagCURSORINFO { + DWORD cbSize; // Size: 0x4 + DWORD flags; // Size: 0x4 + HCURSOR hCursor; // Size: 0x4 (32bit) , 0x8 (64bit) + POINT ptScreenPos; // Size: 0x8 + } CURSORINFO, *PCURSORINFO, *LPCURSORINFO; + Total Size of Structure: + - [32bit] 20 Bytes + - [64bit] 24 Bytes + #> + + # sizeof(cbSize) + sizeof(flags) + sizeof(ptScreenPos) = 16 + $structSize = [IntPtr]::Size + 16 + + $cursorInfo = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($structSize) + try + { + # ZeroMemory(@cursorInfo, SizeOf(tagCURSORINFO)) + for ($i = 0; $i -lt $structSize; $i++) + { + [System.Runtime.InteropServices.Marshal]::WriteByte($cursorInfo, $i, 0x0) + } + + [System.Runtime.InteropServices.Marshal]::WriteInt32($cursorInfo, 0x0, $structSize) + + if ([W.User32]::GetCursorInfo($cursorInfo)) + { + $hCursor = [System.Runtime.InteropServices.Marshal]::ReadInt64($cursorInfo, 0x8) + + return $hCursor + } + + <#for ($i = 0; $i -lt $structSize; $i++) + { + $offsetValue = [System.Runtime.InteropServices.Marshal]::ReadByte($cursorInfo, $i) + Write-Host "Offset: ${i} -> " -NoNewLine + Write-Host $offsetValue -ForegroundColor Green -NoNewLine + Write-Host ' (' -NoNewLine + Write-Host ('0x{0:x}' -f $offsetValue) -ForegroundColor Cyan -NoNewLine + Write-Host ')' + }#> + } + finally + { + [System.Runtime.InteropServices.Marshal]::FreeHGlobal($cursorInfo) + } + } + + 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.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) + + return $true + } + catch + { + return $false + } + } + + $cursors = Initialize-Cursors + + $oldCursor = 0 + + $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() + } + } + + # Monitor for global mouse cursor change + # Update Frequently (Maximum probe time to be efficient: 30ms) + $currentCursor = Get-GlobalMouseCursorIconHandle + if ($currentCursor -ne 0 -and $currentCursor -ne $oldCursor) + { + $cursorTypeName = ($cursors.GetEnumerator() | Where-Object { $_.Value -eq $currentCursor }).Key + + $data = New-Object -TypeName PSCustomObject -Property @{ + Cursor = $cursorTypeName + } + + if (-not (Send-Event -AEvent ([OutputEvent]::MouseCursorUpdated) -Data $data)) + { break } + + $oldCursor = $currentCursor + } + + Start-Sleep -Milliseconds 30 + } + + $stopWatch.Stop() +} + +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 + A PowerShell block of code to be evaluated on the new Runspace. + + .PARAMETER Param + Optional extra parameters to be attached to Runspace. + + .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 + } +} + + +class ClientIO { + [System.Net.Sockets.TcpClient] $Client = $null + [System.IO.StreamWriter] $Writer = $null + [System.IO.StreamReader] $Reader = $null + [System.Net.Security.SslStream] $SSLStream = $null + + + ClientIO( + [System.Net.Sockets.TcpClient] $Client, + [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, + [bool] $TLSv1_3 + ) { + if ((-not $Client) -or (-not $Certificate)) + { + throw "ClientIO Class requires both a valid TcpClient and X509Certificate2." + } + + $this.Client = $Client + + Write-Verbose "Create new SSL Stream..." + + $this.SSLStream = New-Object System.Net.Security.SslStream($this.Client.GetStream(), $false) + + if ($TLSv1_3) + { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 + } + else { + $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 } - if ($this.Stream) + Write-Verbose "Authenticate as server using ${TLSVersion}..." + + $this.SSLStream.AuthenticateAsServer( + $Certificate, + $false, + $TLSVersion, + $false + ) + + if (-not $this.SSLStream.IsEncrypted) { - $this.Stream.Close() + throw "Could not established an encrypted tunnel with remote peer." } - if ($this.Client) - { - $this.Client.Close() - } - } -} + $this.SSLStream.WriteTimeout = 5000 + $this.SSLStream.ReadTimeout = [System.Threading.Timeout]::Infinite # Default -class ServerIO { - [string] $ListenAddress = "127.0.0.1" - [int] $ListenPort = 2801 - [bool] $TLSv1_3 = $false - [string] $Password - [bool] $ViewOnly = $false + Write-Verbose "Open communication channels..." - [System.Net.Sockets.TcpListener] $Server = $null - [System.IO.StreamWriter] $Writer = $null - [System.IO.StreamReader] $Reader = $null - [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate = $null + $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) + $this.Writer.AutoFlush = $true - [ServerSession] $Session = $null + $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) + + Write-Verbose "Connection ready for use." + } - ServerIO( + [bool] Authentify([string] $Password) { <# .SYNOPSIS - Class constructor. - - .PARAMETER ListenAddress - Define in which interface to listen. - - 127.0.0.1: Listen on localhost only. - 0.0.0.0: Listen on all interfaces. + Handle authentication process with remote peer. .PARAMETER Password - Password used to authentify with remote peer. + Password used to validate challenge and grant access for a new Client. - .PARAMETER ListenPort - Define which TCP port to listen for new connection. + .EXAMPLE + .Authentify("s3cr3t!") + #> + try + { + if (-not $Password) { + throw "During client authentication, a password cannot be blank." + } - .PARAMETER Certificate - X509 Certificate used for SSL/TLS encryption tunnel. + Write-Verbose "New authentication challenge..." - .PARAMETER TLSv1_3 - Define if TLS v1.3 must be used. + $candidate = -join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}) + $candidate = Get-SHA512FromString -String $candidate - .PARAMETER ViewOnly - Define if mouse / keyboard is authorized. - #> + $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -Password $Password - [string] $ListenAddress, - [int] $ListenPort, - [string] $Password, - [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, - [bool] $TLSv1_3, - [bool] $ViewOnly - ) { - # Check again in current class just in case. - if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) - { - throw "You must use a complex password for Password Authentication." - } + Write-Verbose "@Challenge:" + Write-Verbose "Candidate: ""${candidate}""" + Write-Verbose "Solution: ""${challengeSolution}""" + Write-Verbose "---" - $this.ListenAddress = $ListenAddress - $this.ListenPort = $ListenPort - $this.TLSv1_3 = $TLSv1_3 - $this.Password = $Password - $this.ViewOnly = $ViewOnly + $this.Writer.WriteLine($candidate) - if (-not $Certificate) - { - Write-Verbose "Custom X509 Certificate not specified." + Write-Verbose "Candidate sent to client, waiting for answer..." - $this.Certificate = Get-X509CertificateFromStore - if (-not $this.Certificate) - { - Write-Verbose "Generate and Install a new local X509 Certificate." + $challengeReply = $this.ReadLine(5 * 1000) - New-DefaultX509Certificate - - Write-verbose "Certificate was successfully installed on local machine. Opening..." + Write-Verbose "Replied solution: ""${challengeReply}""" - $this.Certificate = Get-X509CertificateFromStore - if (-not $this.Certificate) - { - throw "Could not open our new local certificate." - } + # Challenge solution is a Sha512 Hash so comparison doesn't need to be sensitive (-ceq or -cne) + if ($challengeReply -ne $challengeSolution) + { + $this.Writer.WriteLine("KO.") + + throw "Client challenge solution does not match our solution." } else - { - Write-Verbose "Default X509 Certificate was specified." + { + $this.Writer.WriteLine("OK.") + + Write-Verbose "Password Authentication Success" + + return 280121 # True } } - else + catch { - $this.Certificate = $Certificate + throw "Password Authentication Failed. Reason: `r`n $($_)" } - - Write-Verbose "@Certificate:" - Write-Verbose $this.Certificate - Write-Verbose "---" } - [void]Listen() { - <# - .SYNOPSIS - Start listening on defined interface:port. - #> - Write-Verbose "Listen on ""$($this.ListenAddress):$($this.ListenPort)""..." + [string] RemoteAddress() { + return $this.Client.Client.RemoteEndPoint.Address + } - $this.Server = New-Object System.Net.Sockets.TcpListener($this.ListenAddress, $this.ListenPort) + [int] RemotePort() { + return $this.Client.Client.RemoteEndPoint.Port + } - $this.Server.Start(2) # We are only waiting for two clients at the same time. + [string] LocalAddress() { + return $this.Client.Client.LocalEndPoint.Address + } - Write-Verbose "Listening..." + [int] LocalPort() { + return $this.Client.Client.LocalEndPoint.Port } - [ClientIO]PullClient([int]$Timeout) { + [string] ReadLine([int] $Timeout) + { <# .SYNOPSIS - Accept new client and associate this client with a new ClientIO Object. + Read string message from remote peer with timeout support. .PARAMETER Timeout - By default AcceptTcpClient() will block current thread until a client connects. - - Using Timeout and a cool technique, you can stop waiting for client after a certain amount - of time (In Milliseconds) - - If Timeout is greater than 0 (Milliseconds) then connection timeout is enabled. + Define the maximum time (in milliseconds) to wait for remote peer message. #> - - Write-Verbose "Pull Request..." - - if ($Timeout -gt 0) - { - $socketReadList = [System.Collections.ArrayList]@($this.Server.Server) - - [System.Net.Sockets.Socket]::Select($socketReadList, $null, $null, $Timeout * 1000) - - if (-not $socketReadList.Contains($this.Server.Server)) - { - throw "Pull client timeout." - } - } - - $socket = $this.Server.AcceptTcpClient() - - $client = [ClientIO]::New( - $socket, - $this.Certificate, - $this.TLSv1_3 - ) + $defautTimeout = $this.SSLStream.ReadTimeout try - { - Write-Verbose "New client socket connected: ""$($client.RemoteAddress())"". Proceed password authentication..." - - # STEP 1 : Authentication - # When Password Authentication Fail, it throw an exception. But as someone paranoid I also want to be - # that function returns magic token. - $authenticated = ($client.Authentify($this.Password) -eq 280121) - if (-not $authenticated) - { - throw "Access Denied." - } - - if ($this.Session) - { - # STEP 2 : Session Authentication - $client.Hello($this.Session) - } - else - { - # STEP 2 : Create new Session - $this.Session = $client.Hello($this.ViewOnly) - } - } - catch { - $this.CloseSession() - - $client.Close() + $this.SSLStream.ReadTimeout = $Timeout - throw $_ + return $this.Reader.ReadLine() } - - return $client + finally + { + $this.SSLStream.ReadTimeout = $defautTimeout + } } - [void]CloseSession() { + [string] ReadLine() + { <# - Terminate an active Server Session + .SYNOPSIS + Shortcut to Reader ReadLine method. No timeout support. #> - $this.Session = $null + return $this.Reader.ReadLine() } - [void]Close() { + [void] WriteJson([PSCustomObject] $Object) + { <# .SYNOPSIS - Stop waiting for new clients (Stop listening) + Transform a PowerShell Object as a JSON Representation then send to remote + peer. + + .PARAMETER Object + A PowerShell Object to be serialized as JSON String. #> - if ($this.Server) - { - Write-Verbose "Stop listening." - $this.Server.Stop() - } + $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) } -} -$global:DesktopStreamScriptBlock = { - - function Get-DesktopImage { + [void] WriteLine([string] $Value) + { + $this.Writer.WriteLine($Value) + } + + [PSCustomObject] ReadJson([int] $Timeout) + { <# .SYNOPSIS - Return a snapshot of primary screen desktop. + Read json string from remote peer and attempt to deserialize as a PowerShell Object. - .PARAMETER Screen - Define target screen to capture (if multiple monitor exists). - Default is primary screen + .PARAMETER Timeout + Define the maximum time (in milliseconds) to wait for remote peer message. #> - param ( - [System.Windows.Forms.Screen] $Screen = $null - ) - try - { - if (-not $Screen) - { - $Screen = [System.Windows.Forms.Screen]::PrimaryScreen - } - - $size = New-Object System.Drawing.Size( - $Screen.Bounds.Size.Width, - $Screen.Bounds.Size.Height - ) + return ($this.ReadLine($Timeout) | ConvertFrom-Json) + } - $location = New-Object System.Drawing.Point( - $Screen.Bounds.Location.X, - $Screen.Bounds.Location.Y - ) + [PSCustomObject] ReadJson() + { + <# + .SYNOPSIS + Alternative to ReadJson without timeout support. + #> + return ($this.ReadLine() | ConvertFrom-Json) + } - $bitmap = New-Object System.Drawing.Bitmap( - $size.Width, - $size.Height, - [System.Drawing.Imaging.PixelFormat]::Format24bppRgb - ) + [void] Close() { + <# + .SYNOPSIS + Release streams and client. + #> + + if ($this.Writer) + { + $this.Writer.Close() + } - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - - $graphics.CopyFromScreen($location, [System.Drawing.Point]::Empty, $size) - - return $bitmap - } - catch + if ($this.Reader) { - if ($bitmap) - { - $bitmap.Dispose() - } + $this.Reader.Close() } - finally + + if ($this.Stream) { - if ($graphics) - { - $graphics.Dispose() - } + $this.Stream.Close() } - } - $imageQuality = 100 - if ($Param.ImageQuality -ge 0 -and $Param.ImageQuality -lt 100) - { - $imageQuality = $Param.ImageQuality + if ($this.Client) + { + $this.Client.Close() + } } - try +} + +class ServerIO { + [System.Net.Sockets.TcpListener] $Server = $null + [System.IO.StreamWriter] $Writer = $null + [System.IO.StreamReader] $Reader = $null + + ServerIO() { - [System.IO.MemoryStream] $oldImageStream = New-Object System.IO.MemoryStream + + } - $jpegEncoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }; + [void] Listen( + [string] $ListenAddress, + [int] $ListenPort + ) + { + if ($this.Server) + { + $this.Close() + } - $encoderParameters = New-Object System.Drawing.Imaging.EncoderParameters(1) - $encoderParameters.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, $imageQuality) + $this.Server = New-Object System.Net.Sockets.TcpListener( + $ListenAddress, + $ListenPort + ) + + $this.Server.Start() - $packetSize = 9216 # 9KiB - - while ($global:HostSyncHash.RunningSession) - { - try - { - $desktopImage = Get-DesktopImage -Screen $Param.Screen + Write-Verbose "Listening on ""$($ListenAddress):$($ListenPort)""..." + } - $imageStream = New-Object System.IO.MemoryStream + [ClientIO] PullClient( + [string] $Password, - $desktopImage.Save($imageStream, $jpegEncoder, $encoderParameters) + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, - $sendUpdate = $true + [bool] $UseTLSv13, + [int] $Timeout + ) { + <# + .SYNOPSIS + Accept new client and associate this client with a new ClientIO Object. - # Check both stream size. - $sendUpdate = ($oldImageStream.Length -ne $imageStream.Length) + .PARAMETER Timeout + By default AcceptTcpClient() will block current thread until a client connects. + + Using Timeout and a cool technique, you can stop waiting for client after a certain amount + of time (In Milliseconds) - # If sizes are equal, compare both Fingerprint to confirm finding. - if (-not $sendUpdate) - { - $imageStream.position = 0 - $oldImageStream.position = 0 + If Timeout is greater than 0 (Milliseconds) then connection timeout is enabled. - $md5_1 = (Get-FileHash -InputStream $imageStream -Algorithm MD5).Hash - $md5_2 = (Get-FileHash -InputStream $oldImageStream -Algorithm MD5).Hash + Other method: AsyncWaitHandle.WaitOne([timespan])'h:m:s') -eq $true|$false with BeginAcceptTcpClient(...) + #> + + if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) + { + throw "Client socket pull request requires a complex password." + } - $sendUpdate = ($md5_1 -ne $md5_2) - } + if ($Timeout -gt 0) + { + $socketReadList = [System.Collections.ArrayList]@($this.Server.Server) - if ($sendUpdate) - { - $imageStream.position = 0 - try - { - $Param.Client.SSLStream.Write([BitConverter]::GetBytes([int32] $imageStream.Length) , 0, 4) # SizeOf(Int32) - - $binaryReader = New-Object System.IO.BinaryReader($imageStream) - do - { - $bufferSize = ($imageStream.Length - $imageStream.Position) - if ($bufferSize -gt $packetSize) - { - $bufferSize = $packetSize - } + [System.Net.Sockets.Socket]::Select($socketReadList, $null, $null, $Timeout * 1000) - $Param.Client.SSLStream.Write($binaryReader.ReadBytes($bufferSize), 0, $bufferSize) - } until ($imageStream.Position -eq $imageStream.Length) - } - catch - { break } + if (-not $socketReadList.Contains($this.Server.Server)) + { + throw "Pull timeout." + } + } - # Update Old Image Stream for Comparison - $imageStream.position = 0 + $socket = $this.Server.AcceptTcpClient() - $oldImageStream.SetLength(0) + $client = [ClientIO]::New( + $socket, + $Certificate, + $UseTLSv13 + ) + try + { + Write-Verbose "New client socket connected from: ""$($client.RemoteAddress())""." - $imageStream.CopyTo($oldImageStream) - } - else {} - } - catch - { } - finally + $authenticated = ($client.Authentify($Password) -eq 280121) + if (-not $authenticated) { - if ($desktopImage) - { - $desktopImage.Dispose() - } + throw "Access Denied." + } + } + catch + { + $client.Close() - if ($imageStream) - { - $imageStream.Close() - } - } + throw $_ } + + return $client } - finally + + [bool] Active() { - if ($oldImageStream) + if ($this.Server) { - $oldImageStream.Close() + return $this.Server.Active + } + else { + return $false } } -} -$global:IngressEventScriptBlock = { + [void] Close() + { + <# + .SYNOPSIS + Stop listening and release TcpListener object. + #> + if ($this.Server) + { + if ($this.Server.Active) + { + $this.Server.Stop() + } - Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern void mouse_event(int flags, int dx, int dy, int cButtons, int info);[DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);' -Name U32 -Namespace W; + $this.Server = $null - enum MouseFlags { - MOUSEEVENTF_ABSOLUTE = 0x8000 - MOUSEEVENTF_LEFTDOWN = 0x0002 - MOUSEEVENTF_LEFTUP = 0x0004 - MOUSEEVENTF_MIDDLEDOWN = 0x0020 - MOUSEEVENTF_MIDDLEUP = 0x0040 - MOUSEEVENTF_MOVE = 0x0001 - MOUSEEVENTF_RIGHTDOWN = 0x0008 - MOUSEEVENTF_RIGHTUP = 0x0010 - MOUSEEVENTF_WHEEL = 0x0800 - MOUSEEVENTF_XDOWN = 0x0080 - MOUSEEVENTF_XUP = 0x0100 - MOUSEEVENTF_HWHEEL = 0x01000 + Write-Verbose "Server is now released." + } } +} - enum InputEvent { - Keyboard = 0x1 - MouseClickMove = 0x2 - MouseWheel = 0x3 - KeepAlive = 0x4 - ClipboardUpdated = 0x5 +class ViewerConfiguration { + [string] $ScreenName = "" + [int] $ExpectDesktopWidth = 0 + [int] $ExpectDesktopHeight = 0 + [int] $ImageCompressionQuality = 100 + + [bool] ResizeDesktop() + { + return $this.ExpectDesktopHeight -gt 0 -or $this.ExpectDesktopWidth -gt 0 } - enum MouseState { - Up = 0x1 - Down = 0x2 - Move = 0x3 + [void] SetImageCompressionQuality([int] $Value) + { + if ($Value -lt 0) + { + $Value = 0 + } + + if ($Value -gt 100) + { + $Value = 100 + } + + $this.ImageCompressionQuality = $Value } +} - enum ClipboardMode { - Disabled = 1 - Receive = 2 - Send = 3 - Both = 4 +class ServerSession { + [string] $Id = "" + [bool] $ViewOnly = $false + [ClipboardMode] $Clipboard = [ClipboardMode]::Both + + [System.Collections.Generic.List[PSCustomObject]] + $WorkerThreads = @() + + [System.Collections.Generic.List[ClientIO]] + $Clients = @() + + $SafeHash = [HashTable]::Synchronized(@{ + ViewerConfiguration = [ViewerConfiguration]::New() + }) + + ServerSession( + [bool] $ViewOnly, + [ClipboardMode] $Clipboard + ) + { + $this.Id = (SHA512FromString -String (-join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}))) + + $this.ViewOnly = $ViewOnly + $this.Clipboard = $Clipboard } - class KeyboardSim { + [bool] CompareSession([string] $Id) + { <# .SYNOPSIS - Class to simulate Keyboard Events using a WScript.Shell - Instance. + Compare two session object. In this case just compare session id string. + + .PARAMETER Id + A session id to compare with current session object. #> - [System.__ComObject] $WShell = $null + return ($this.Id -ceq $Id) + } - KeyboardSim () + [void] NewDesktopWorker([ClientIO] $Client) + { <# .SYNOPSIS - Class constructor + Create a new desktop streaming worker (Runspace/Thread). + + .PARAMETER Client + An established connection with remote peer as a ClientIO Object. #> - { - $this.WShell = New-Object -ComObject WScript.Shell + $param = New-Object -TypeName PSCustomObject -Property @{ + Client = $Client + SafeHash = $this.SafeHash } + + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:DesktopStreamScriptBlock -Param $param)) + + ### - [void] SendInput([string] $String) - { - <# - .SYNOPSIS - Simulate Keyboard Strokes. It can contain a single char or a complex string. + $this.Clients.Add($Client) + } - .PARAMETER String - Char or String to be simulated as pressed. + [void] NewEventWorker([ClientIO] $Client) + { + <# + .SYNOPSIS + Create a new egress / ingress worker (Runspace/Thread) to process outgoing / incomming events. - .EXAMPLE - .SendInput("Hello, World") - .SendInput("J") - #> + .PARAMETER Client + An established connection with remote peer as a ClientIO Object. + #> - # Simulate - $this.WShell.SendKeys($String) + $param = New-Object -TypeName PSCustomObject -Property @{ + Writer = $Client.Writer + Clipboard = $this.Clipboard } - } - - $keyboardSim = [KeyboardSim]::New() - while ($global:HostSyncHash.RunningSession) - { - try - { - $jsonEvent = $Param.Reader.ReadLine() - } - catch - { - # ($_ | Out-File "c:\temp\debug.txt") - - break - } + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param)) + + ### - try - { - $aEvent = $jsonEvent | ConvertFrom-Json + $param = New-Object -TypeName PSCustomObject -Property @{ + Reader = $Client.Reader + Clipboard = $this.Clipboard + ViewOnly = $this.ViewOnly } - catch { continue } - - if (-not ($aEvent.PSobject.Properties.name -match "Id")) - { continue } - - switch ([InputEvent] $aEvent.Id) - { - # Keyboard Input Simulation - ([InputEvent]::Keyboard) - { - if ($Param.ViewOnly) - { continue } - - if (-not ($aEvent.PSobject.Properties.name -match "Keys")) - { break } + + $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param)) - $keyboardSim.SendInput($aEvent.Keys) - break - } + ### - # Mouse Move & Click Simulation - ([InputEvent]::MouseClickMove) - { - if ($Param.ViewOnly) - { continue } + $this.Clients.Add($Client) + } - if (-not ($aEvent.PSobject.Properties.name -match "Type")) - { break } + [void] Close() + { + <# + .SYNOPSIS + Close components associated with current session (Ex: runspaces, sockets etc..) + #> - switch ([MouseState] $aEvent.Type) - { - # Mouse Down/Up - {($_ -eq ([MouseState]::Down)) -or ($_ -eq ([MouseState]::Up))} - { - [W.U32]::SetCursorPos($aEvent.X, $aEvent.Y) + # Close connection with remote peers associated with this session + foreach ($client in $this.Clients) + { + $client.Close() + } - $down = ($_ -eq ([MouseState]::Down)) + $this.Clients.Clear() - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTDOWN - if (-not $down) - { - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_LEFTUP - } + # TODO: wait for workers to finish their jobs - switch($aEvent.Button) - { - "Right" - { - if ($down) - { - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTDOWN - } - else - { - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_RIGHTUP - } + # Terminate runspaces associated with this session + foreach ($worker in $this.WorkerThreads) + { + $null = $worker.PowerShell.EndInvoke($worker.AsyncResult) + $worker.PowerShell.Runspace.Dispose() + $worker.PowerShell.Dispose() + } + $this.WorkerThreads.Clear() + } +} - break - } +class SessionManager { + [ServerIO] $Server = $null - "Middle" - { - if ($down) - { - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEDOWN - } - else - { - $mouseCode = [int][MouseFlags]::MOUSEEVENTF_MIDDLEUP - } - } - } - [W.U32]::mouse_event($mouseCode, 0, 0, 0, 0); + [System.Collections.Generic.List[ServerSession]] + $Sessions = @() - break - } + [string] $Password = "" - # Mouse Move - ([MouseState]::Move) - { - if ($Param.ViewOnly) - { continue } + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate = $null - [W.U32]::SetCursorPos($aEvent.X, $aEvent.Y) + [bool] $ViewOnly = $false + [bool] $UseTLSv13 = $false - break - } - } + [ClipboardMode] $Clipboard = [ClipboardMode]::Both - break - } + SessionManager( + [string] $Password, - # Mouse Wheel Simulation - ([InputEvent]::MouseWheel) { - if ($Param.ViewOnly) - { continue } + [System.Security.Cryptography.X509Certificates.X509Certificate2] + $Certificate, - [W.U32]::mouse_event([int][MouseFlags]::MOUSEEVENTF_WHEEL, 0, 0, $aEvent.Delta, 0); + [bool] $ViewOnly, + [bool] $UseTLSv13, + [ClipboardMode] $Clipboard + ) + { + Write-Verbose "Initialize new session manager..." - break - } + if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) + { + throw "Session manager requires a complex password for viewer-authentication." + } - # Clipboard Update - ([InputEvent]::ClipboardUpdated) - { - if ($Param.Clipboard -eq ([ClipboardMode]::Disabled) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) - { continue } + $this.Password = $Password + $this.ViewOnly = $ViewOnly + $this.UseTLSv13 = $UseTLSv13 + $this.Clipboard = $Clipboard - if (-not ($aEvent.PSobject.Properties.name -match "Text")) - { continue } + if (-not $Certificate) + { + Write-Verbose "Custom X509 Certificate not specified." - $HostSyncHash.ClipboardText = $aEvent.Text + $this.Certificate = Get-X509CertificateFromStore + if (-not $this.Certificate) + { + Write-Verbose "Generate and Install a new local X509 Certificate." + + New-DefaultX509Certificate - Set-Clipboard -Value $aEvent.Text + Write-verbose "Certificate was successfully installed on local machine. Opening..." + + $this.Certificate = Get-X509CertificateFromStore + if (-not $this.Certificate) + { + throw "Could not open our new local certificate." + } + } + else + { + Write-Verbose "Default X509 Certificate was specified." } } - } -} + else + { + $this.Certificate = $Certificate + } -$global:EgressEventScriptBlock = { + Write-Verbose "@Certificate:" + Write-Verbose $this.Certificate + Write-Verbose "---" - enum CursorType { - IDC_APPSTARTING = 32650 - IDC_ARROW = 32512 - IDC_CROSS = 32515 - IDC_HAND = 32649 - IDC_HELP = 32651 - IDC_IBEAM = 32513 - IDC_ICON = 32641 - IDC_NO = 32648 - IDC_SIZE = 32640 - IDC_SIZEALL = 32646 - IDC_SIZENESW = 32643 - IDC_SIZENS = 32645 - IDC_SIZENWSE = 32642 - IDC_SIZEWE = 32644 - IDC_UPARROW = 32516 - IDC_WAIT = 32514 + Write-Verbose "Session manager initialized." } - enum OutputEvent { - KeepAlive = 0x1 - MouseCursorUpdated = 0x2 - ClipboardUpdated = 0x3 - } + [void] OpenServer( + [string] $ListenAddress, + [int] $ListenPort + ) + { + <# + .SYNOPSIS + Create a new server object then start listening on desired interface / port. - enum ClipboardMode { - Disabled = 1 - Receive = 2 - Send = 3 - Both = 4 + .PARAMETER ListenAddress + Desired interface to listen for new peers. + "127.0.0.1" = Only listen for localhost peers. + "0.0.0.0" = Listen on all interfaces for peers. + + .PARAMETER ListenPort + TCP Port to listen for new peers (0-65535) + #> + + $this.CloseServer() + try + { + $this.Server = [ServerIO]::New() + + $this.Server.Listen( + $ListenAddress, + $ListenPort + ) + } + catch + { + $this.CloseServer() + + throw $_ + } } - function Initialize-Cursors + [ServerSession] GetSession([string] $SessionId) { <# .SYNOPSIS - Initialize different Windows supported mouse cursors. - - .DESCRIPTION - Unfortunately, there is not WinAPI to get current mouse cursor icon state (Ex: as a flag) - but only current mouse cursor icon (via its handle). + Find a session by its id on current session pool. - One solution, is to resolve each supported mouse cursor handles (HCURSOR) with corresponding name - in a hashtable and then compare with GetCursorInfo() HCURSOR result. + .PARAMETER SessionId + Session id to search in current pool. #> - $cursors = @{} - - foreach ($cursorType in [CursorType].GetEnumValues()) { - $result = [W.User32]::LoadCursorA(0, [int]$cursorType) - - if ($result -gt 0) + foreach ($session in $this.Sessions) + { + if ($session.CompareSession($SessionId)) { - $cursors[[string] $cursorType] = $result + return $session } } - return $cursors - } + return $null + } - function Get-GlobalMouseCursorIconHandle - { + [void] ProceedNewSessionRequest([ClientIO] $Client) + { <# .SYNOPSIS - Return global mouse cursor handle. + Attempt a new session request with remote peer. + .DESCRIPTION - For this project I really want to avoid using "inline c#" but only pure PowerShell Code. - I'm using a Hackish method to retrieve the global Windows cursor info by playing by hand - with memory to prepare and read CURSORINFO structure. - --- - typedef struct tagCURSORINFO { - DWORD cbSize; // Size: 0x4 - DWORD flags; // Size: 0x4 - HCURSOR hCursor; // Size: 0x4 (32bit) , 0x8 (64bit) - POINT ptScreenPos; // Size: 0x8 - } CURSORINFO, *PCURSORINFO, *LPCURSORINFO; - Total Size of Structure: - - [32bit] 20 Bytes - - [64bit] 24 Bytes - #> + Session creation is now requested from a dedicated client instead of using + same client as for desktop streaming. - # sizeof(cbSize) + sizeof(flags) + sizeof(ptScreenPos) = 16 - $structSize = [IntPtr]::Size + 16 + I prefer to use a dedicated client to have a more cleaner session establishement + process. - $cursorInfo = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($structSize) + Session request will basically generate a new session object, send some information + about current server marchine state then wait for viewer acknowledgement with desired + configuration (Ex: desired screen to capture, quality and local size constraints). + + When session creation is done, client is then closed. + + #> try - { - # ZeroMemory(@cursorInfo, SizeOf(tagCURSORINFO)) - for ($i = 0; $i -lt $structSize; $i++) - { - [System.Runtime.InteropServices.Marshal]::WriteByte($cursorInfo, $i, 0x0) - } + { + Write-Verbose "Remote peer as requested a new session..." - [System.Runtime.InteropServices.Marshal]::WriteInt32($cursorInfo, 0x0, $structSize) + $session = [ServerSession]::New($this.ViewOnly, $this.Clipboard) - if ([W.User32]::GetCursorInfo($cursorInfo)) - { - $hCursor = [System.Runtime.InteropServices.Marshal]::ReadInt64($cursorInfo, 0x8) + Write-Verbose "@ServerSession" + Write-Verbose "Id: ""$($session.Id)""" + Write-Verbose "---" - return $hCursor - } + $serverInformation = New-Object PSCustomObject -Property @{ + # Session information and configuration + SessionId = $session.Id + Version = $global:PowerRemoteDesktopVersion + ViewOnly = $this.ViewOnly + + # Local machine information + MachineName = [Environment]::MachineName + Username = [Environment]::UserName + WindowsVersion = [Environment]::OSVersion.VersionString + Screens = Get-ScreenList + } - <#for ($i = 0; $i -lt $structSize; $i++) - { - $offsetValue = [System.Runtime.InteropServices.Marshal]::ReadByte($cursorInfo, $i) - Write-Host "Offset: ${i} -> " -NoNewLine - Write-Host $offsetValue -ForegroundColor Green -NoNewLine - Write-Host ' (' -NoNewLine - Write-Host ('0x{0:x}' -f $offsetValue) -ForegroundColor Cyan -NoNewLine - Write-Host ')' - }#> - } - finally - { - [System.Runtime.InteropServices.Marshal]::FreeHGlobal($cursorInfo) - } - } + Write-Verbose "Sending server information to remote peer..." - function Send-Event - { - <# - .SYNOPSIS - Send an event to remote peer. + Write-Verbose "@ServerInformation:" + Write-Verbose $serverInformation + Write-Verbose "---" - .PARAMETER AEvent - Define what kind of event to send. + $client.WriteJson($serverInformation) - .PARAMETER Data - An optional object containing additional information about the event. - #> - param ( - [Parameter(Mandatory=$True)] - [OutputEvent] $AEvent, + Write-Verbose "Waiting for viewer expectation..." + + if ($serverInformation.Screens.Length -gt 1) + { + # Client have a maximum of 1 Minute to reply with viewer expectation. + # This timeout is high to give enough time to the end-user to choose which screen he wants to use + $timeout = 60 * 1000 + } + else + { + $timeout = 5 * 1000 + } - [PSCustomObject] $Data = $null - ) + $viewerExpectation = $client.ReadJson($timeout) - try - { - if (-not $Data) + if ($viewerExpectation.PSobject.Properties.name -contains "ScreenName") + { + $session.SafeHash.ViewerConfiguration.ScreenName = $viewerExpectation.ScreenName + } + + if ($viewerExpectation.PSobject.Properties.name -contains "ExpectDesktopWidth") + { + $session.SafeHash.ViewerConfiguration.ExpectDesktopWidth = $viewerExpectation.ExpectDesktopWidth + } + + if ($viewerExpectation.PSobject.Properties.name -contains "ExpectDesktopHeight") { - $Data = New-Object -TypeName PSCustomObject -Property @{ - Id = $AEvent - } + $session.SafeHash.ViewerConfiguration.ExpectDesktopHeight = $viewerExpectation.ExpectDesktopHeight } - else + + if ($viewerExpectation.PSobject.Properties.name -contains "ImageCompressionQuality") { - $Data | Add-Member -MemberType NoteProperty -Name "Id" -Value $AEvent - } + $session.SafeHash.ViewerConfiguration.ImageCompressionQuality = $viewerExpectation.ImageCompressionQuality + } - $Param.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) + Write-Verbose "New session successfully created." - return $true - } - catch - { - return $false + $this.Sessions.Add($session) + + $client.WriteLine("OK") } - } + catch + { + $session = $null - $cursors = Initialize-Cursors + throw $_ + } + finally + { + if ($client) + { + $client.Close() + } + } + } - $oldCursor = 0 + [void] ProceedAttachRequest([ClientIO] $Client) + { + <# + .SYNOPSIS + Attach a new peer to an existing session then dispatch this new peer as a + new stateful worker. - $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + .PARAMETER Client + An established connection with remote peer as a ClientIO Object. + #> + Write-Verbose "Proceed new session attach request..." - while ($global:HostSyncHash.RunningSession) - { - # 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) + $session = $this.GetSession($Client.ReadLine(5 * 1000)) + if (-not $session) { - try - { - $eventTriggered = $false + $Client.WriteLine("SESS_NOTFOUND") - 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) + throw "Session object matchin given id could not be find in active session pool." + } - if ($currentClipboard -and $currentClipboard -cne $HostSyncHash.ClipboardText) - { - $data = New-Object -TypeName PSCustomObject -Property @{ - Text = $currentClipboard - } + Write-Verbose "Client successfully attached to session: ""$($session.id)""" - if (-not (Send-Event -AEvent ([OutputEvent]::ClipboardUpdated) -Data $data)) - { break } + $Client.WriteLine("SESS_OK") - $HostSyncHash.ClipboardText = $currentClipboard + $workerKind = $Client.ReadLine(5 * 1000) - $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 + switch ($workerKind) + { + "WRKER_DESKTOP" { - $stopWatch.Restart() + $session.NewDesktopWorker($Client) + + break } - } - # Monitor for global mouse cursor change - # Update Frequently (Maximum probe time to be efficient: 30ms) - $currentCursor = Get-GlobalMouseCursorIconHandle - if ($currentCursor -ne 0 -and $currentCursor -ne $oldCursor) - { - $cursorTypeName = ($cursors.GetEnumerator() | Where-Object { $_.Value -eq $currentCursor }).Key + "WRKER_EVENT" + { + $session.NewEventWorker($Client) # I/O - $data = New-Object -TypeName PSCustomObject -Property @{ - Cursor = $cursorTypeName - } + break + } + } + } + + [void] ListenForWorkers() + { + while ($true) + { + if (-not $this.Server -or $this.Server.Active()) + { + throw "A server must be active to listen for new workers." + } - if (-not (Send-Event -AEvent ([OutputEvent]::MouseCursorUpdated) -Data $data)) - { break } + $client = $null + try + { + $client = $this.Server.PullClient( + $this.Password, + $this.Certificate, + $this.UseTLSv13, + 5 * 1000 + ) - $oldCursor = $currentCursor - } + $requestMode = $client.ReadLine(5 * 1000) - Start-Sleep -Milliseconds 30 - } + switch ($requestMode) + { + "REQ_SESSION" + { + $this.ProceedNewSessionRequest($client) - $stopWatch.Stop() -} + break + } -function New-RunSpace -{ - <# - .SYNOPSIS - Create a new PowerShell Runspace. + "REQ_ATTACH" + { + $this.ProceedAttachRequest($client) - .DESCRIPTION - Notice: the $host variable is used for debugging purpose to write on caller PowerShell - Terminal. + break + } - .PARAMETER ScriptBlock - A PowerShell block of code to be evaluated on the new Runspace. + default: + { + $client.WriteLine("BAD_REQ") - .PARAMETER Param - Optional extra parameters to be attached to Runspace. + throw "Bad request." + } + } + } + catch + { + if ($client) + { + $client.Close() - .EXAMPLE - New-RunSpace -Client $newClient -ScriptBlock { Start-Sleep -Seconds 10 } - #> + $client = $null + } + } + finally + { } + } + } - param( - [Parameter(Mandatory=$True)] - [ScriptBlock] $ScriptBlock, + [void] CloseServer() + { + <# + .SYNOPSIS + Close all existing sessions and dispose server. + #> - [PSCustomObject] $Param = $null - ) + $this.CloseSessions() - $runspace = [RunspaceFactory]::CreateRunspace() - $runspace.ThreadOptions = "ReuseThread" - $runspace.ApartmentState = "STA" - $runspace.Open() + if ($this.Server) + { + $this.Server.Close() - if ($Param) - { - $runspace.SessionStateProxy.SetVariable("Param", $Param) + $this.Server = $null + } } - $runspace.SessionStateProxy.SetVariable("HostSyncHash", $global:HostSyncHash) - - $powershell = [PowerShell]::Create().AddScript($ScriptBlock) - - $powershell.Runspace = $runspace + [void] CloseSessions() + { + <# + .SYNOPSIS + Close all existing sessions + #> - $asyncResult = $powershell.BeginInvoke() + foreach ($session in $this.Sessions) + { + $session.Close() + } - return New-Object PSCustomObject -Property @{ - Runspace = $runspace - PowerShell = $powershell - AsyncResult = $asyncResult + $this.Sessions.Clear() } } @@ -1748,12 +2225,7 @@ function Invoke-RemoteDesktopServer Define whether or not TLS v1.3 must be used for communication with Viewer. .PARAMETER DisableVerbosity - Disable verbosity (not recommended) - - .PARAMETER ImageQuality - JPEG Compression level from 0 to 100 - 0 = Lowest quality. - 100 = Highest quality. + Disable verbosity (not recommended) .PARAMETER Clipboard Define clipboard synchronization rules: @@ -1769,7 +2241,10 @@ function Invoke-RemoteDesktopServer param ( [string] $ListenAddress = "0.0.0.0", + + [ValidateRange(0, 65535)] [int] $ListenPort = 2801, + [string] $Password = "", [string] $CertificateFile = "", # 1 @@ -1778,14 +2253,10 @@ function Invoke-RemoteDesktopServer [switch] $TLSv1_3, [switch] $DisableVerbosity, - [int] $ImageQuality = 100, [ClipboardMode] $Clipboard = [ClipboardMode]::Both, [switch] $ViewOnly ) - - [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() - $oldErrorActionPreference = $ErrorActionPreference $oldVerbosePreference = $VerbosePreference try @@ -1813,13 +2284,19 @@ function Invoke-RemoteDesktopServer Specify your own X509 Certificate or run the server in a elevated prompt." } - if ($CertificateFile) + $Certificate = $null + + if (($CertificateFile -and (Test-Path -Path $CertificateFile)) -or $EncodedCertificate) { - # TODO: Test if certificate is well-formed. + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - if (-not (Test-Path -Path $CertificateFile)) - { - throw "Certificate file not found at location: ""${CertificateFile}""." + if ($CertificateFile) + { + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFile + } + else + { + $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 @(, [Convert]::FromBase64String($EncodedCertificate)) } } @@ -1841,159 +2318,37 @@ function Invoke-RemoteDesktopServer * At least of lower case character`r`n` * At least of upper case character`r`n" } - } + } - $Certificate = $null - - if ($CertificateFile -or $EncodedCertificate) + try { - $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - if ($CertificateFile) - { - $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFile - } - else - { - $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 @(, [Convert]::FromBase64String($EncodedCertificate)) - } - } - - # Create new server and listen - $server = [ServerIO]::New( - $ListenAddress, - $ListenPort, - $Password, - $Certificate, - $TLSv1_3, - $ViewOnly - ) - - $server.Listen() - - while ($true) - { - try - { - $global:HostSyncHash.RunningSession = $false - - Write-Verbose "Server waiting for new incomming session..." - - # Establish a new Remote Desktop Session. - $clientDesktop = $server.PullClient(0); - - # Attach to existing session a new handler. - # An established session is expected to open a new client in the next 10 seconds. - # Otherwise a Timeout Exception will be raised. - # Actually, if someone else decide to connect in the mean time it will interrupt the whole session, - # Remote Viewer will then need to establish a new session from scratch. - $clientEvents = $server.PullClient(10 * 1000); - - $global:HostSyncHash.RunningSession = $true - - # Grab desired screen to capture - $screen = [System.Windows.Forms.Screen]::AllScreens | Where-Object -FilterScript { $_.DeviceName -eq $server.Session.Screen } - - # Create Runspace #1 for Desktop Streaming. - $param = New-Object -TypeName PSCustomObject -Property @{ - Client = $clientDesktop - Screen = $screen - ImageQuality = $ImageQuality - } - - $newRunspace = (New-RunSpace -ScriptBlock $global:DesktopStreamScriptBlock -Param $param) - $runspaces.Add($newRunspace) + $sessionManager = [SessionManager]::New( + $Password, + $Certificate, + $ViewOnly, + $TLSv1_3, + $Clipboard + ) - # Notice: In current PowerRemoteDesktop Protocol design, Client wont Read or Write simultaneously from different - # threads. Sockets allow to Read and Write at the same time but not Read or Write at the same - # time. - - # If protocol change and require simultaneously Read or Write from different threads - # I will need to implement a synchronization mechanism to avoid conflicts like Synchronized Hashtables. - - # Create Runspace #2 for Incoming Events. - $param = New-Object -TypeName PSCustomObject -Property @{ - Reader = $clientEvents.Reader - Clipboard = $Clipboard - ViewOnly = $ViewOnly - } - - $newRunspace = (New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param) - $runspaces.Add($newRunspace) - - # Create Runspace #3 for Outgoing Events - $param = New-Object -TypeName PSCustomObject -Property @{ - Writer = $clientEvents.Writer - Clipboard = $Clipboard - } - - $newRunspace = (New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param) - $runspaces.Add($newRunspace) - - # Waiting for Runspaces to finish their jobs. - while ($true) - { - $completed = $true - - # Probe each existing runspaces - foreach ($runspace in $runspaces) - { - if (-not $runspace.AsyncResult.IsCompleted) - { - $completed = $false - } - elseif ($global:HostSyncHash.RunningSession) - { - # Notifying other runspaces that a session integrity was lost - $global:HostSyncHash.RunningSession = $false - } - } - - if ($completed) - { break } - - Start-Sleep -Seconds 2 - } - } - catch { - Write-Output "Viewer Session Exception Raised:" - Write-Host $_ -ForegroundColor Red - Write-Output "---" - } - finally - { - Write-Verbose "Terminate session and close active connections..." - - $server.CloseSession() - - if ($clientEvents) - { - $clientEvents.Close() - } - - if ($clientDesktop) - { - $clientDesktop.Close() - } - - Write-Verbose "Free runspaces..." - - foreach ($runspace in $runspaces) - { - $null = $runspace.PowerShell.EndInvoke($runspace.AsyncResult) - $runspace.PowerShell.Runspace.Dispose() - $runspace.PowerShell.Dispose() - } - $runspaces.Clear() - } + $sessionManager.OpenServer( + $ListenAddress, + $ListenPort + ) + + $sessionManager.ListenForWorkers() } - } - finally - { - if ($server) + finally { - $server.Close() - } + if ($sessionManager) + { + $sessionManager.CloseServer() + $sessionManager = $null + } + } + } + finally + { $ErrorActionPreference = $oldErrorActionPreference $VerbosePreference = $oldVerbosePreference } diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 200cb95..113378f 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -51,7 +51,7 @@ Add-Type -Assembly System.Windows.Forms Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W; -$global:PowerRemoteDesktopVersion = "1.0.6" +$global:PowerRemoteDesktopVersion = "2.0.0" $global:HostSyncHash = [HashTable]::Synchronized(@{ host = $host @@ -562,155 +562,91 @@ class ClientIO { } - [void]Hello([string] $SessionId) { - <# - .SYNOPSIS - This method must be called after Password-Authentication to finalise an established - connection with server. - - .PARAMETER SessionId - A String containing the Session Id. - #> + [string] RemoteAddress() { + return $this.Client.Client.RemoteEndPoint.Address + } - Write-Verbose "Say Hello..." + [int] RemotePort() { + return $this.Client.Client.RemoteEndPoint.Port + } - $this.Writer.WriteLine($SessionId) + [string] LocalAddress() { + return $this.Client.Client.LocalEndPoint.Address + } - $result = $this.Reader.ReadLine() - if ($result -eq "HELLO.") - { - Write-Verbose "Server Hello back." - } - else - { - throw "Could not finalise connection with remote server. Session Id is wrong or was terminated." - } + [int] LocalPort() { + return $this.Client.Client.LocalEndPoint.Port } - - [PSCustomObject]Hello(){ + + [string] ReadLine([int] $Timeout) + { <# .SYNOPSIS - This method must be called after Password-Authentication to finalise an established - connection with server. + Read string message from remote peer with timeout support. - .DESCRIPTION - This method is called when no session is already present. Server will send several informations - including a new session id the store. - - TODO: Instead of PSCustomObject, create a specific class ? + .PARAMETER Timeout + Define the maximum time (in milliseconds) to wait for remote peer message. #> - - Write-Verbose "Say Hello..." - - $jsonObject = $this.Reader.ReadLine() - - Write-Verbose "@SessionInformation:" - Write-Verbose $jsonObject - Write-Verbose "---" - - $sessionInformation = $jsonObject | ConvertFrom-Json - if ( - (-not ($sessionInformation.PSobject.Properties.name -contains "MachineName")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "Username")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "WindowsVersion")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "SessionId")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "Version")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "Screens")) -or - (-not ($sessionInformation.PSobject.Properties.name -contains "ViewOnly")) - ) - { - throw "Invalid session information data." - } - - if ($sessionInformation.Version -ne $global:PowerRemoteDesktopVersion) + $defautTimeout = $this.SSLStream.ReadTimeout + try { - throw "Server and Viewer version mismatch.`r`n` - Local: ""${global:PowerRemoteDesktopVersion}""`r`n` - Remote: ""$($sessionInformation.Version)""`r`n` - You cannot use two different version between Viewer and Server." - } + $this.SSLStream.ReadTimeout = $Timeout - if ($sessionInformation.ViewOnly) - { - Write-Host "You are only authorized to view remote desktop (Mouse / Keyboard not authorized)" -ForegroundColor Cyan + return $this.Reader.ReadLine() } - - # Check if remote server have multiple screens - $selectedScreen = $null - - if ($sessionInformation.Screens.Length -gt 1) + finally { - Write-Verbose "Remote Server have $($sessionInformation.Screens.Length) Screens." - - Write-Host "Remote Server have " -NoNewLine - Write-Host $($sessionInformation.Screens.Length) -NoNewLine -ForegroundColor Green - Write-Host " Screens:`r`n" - - foreach ($screen in $sessionInformation.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 = $sessionInformation.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 - } + $this.SSLStream.ReadTimeout = $defautTimeout + } + } - $selectedScreen = $sessionInformation.Screens | Where-Object -FilterScript { $_.Id -eq $choice } + [string] ReadLine() + { + <# + .SYNOPSIS + Shortcut to Reader ReadLine method. No timeout support. + #> + return $this.Reader.ReadLine() + } - if (-not $selectedScreen) - { - Write-Host "Invalid choice, please choose an existing screen index." -ForegroundColor Red - } - } + [void] WriteJson([PSCustomObject] $Object) + { + <# + .SYNOPSIS + Transform a PowerShell Object as a JSON Representation then send to remote + peer. - if ($selectedScreen) - { - $this.Writer.WriteLine($selectedScreen.Name) + .PARAMETER Object + A PowerShell Object to be serialized as JSON String. + #> - break - } - } - } - else - { - $selectedScreen = $sessionInformation.Screens | Select-Object -First 1 - } + $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) + } - if (-not $selectedScreen) - { - throw "No screen to capture." - } + [void] WriteLine([string] $Value) + { + $this.Writer.WriteLine($Value) + } - Write-Verbose "@SelectedScreen:" - Write-Verbose $selectedScreen - Write-Verbose "---" + [PSCustomObject] ReadJson([int] $Timeout) + { + <# + .SYNOPSIS + Read json string from remote peer and attempt to deserialize as a PowerShell Object. - $sessionInformation | Add-Member -MemberType NoteProperty -Name "Screen" -Value $selectedScreen + .PARAMETER Timeout + Define the maximum time (in milliseconds) to wait for remote peer message. + #> + return ($this.ReadLine($Timeout) | ConvertFrom-Json) + } - return $sessionInformation + [PSCustomObject] ReadJson() + { + <# + .SYNOPSIS + Alternative to ReadJson without timeout support. + #> + return ($this.ReadLine() | ConvertFrom-Json) } [void]Close() { @@ -745,13 +681,24 @@ class ClientIO { } } +class ViewerConfiguration +{ + [int] $VirtualDesktopWidth = 0 + [int] $VirtualDesktopHeight = 0 + [bool] $RequireResize = $false +} + class ViewerSession { - [PSCustomObject] $SessionInformation = $null + [PSCustomObject] $ServerInformation = $null + [ViewerConfiguration] $ViewerConfiguration = $null + [string] $ServerAddress = "127.0.0.1" [string] $ServerPort = 2801 [SecureString] $SecurePassword = $null - [bool] $TLSv1_3 = $false + [bool] $TLSv1_3 = $false + [int] $ImageCompressionQuality = 100 + [int] $ResizeRatio = 0 [ClientIO] $ClientDesktop = $null [ClientIO] $ClientEvents = $null @@ -760,31 +707,10 @@ class ViewerSession [string] $ServerAddress, [int] $ServerPort, [SecureString] $SecurePassword, - [bool] $TLSv1_3 + [bool] $TLSv1_3, + [int] $ImageCompressionQuality ) - { - <# - .SYNOPSIS - Create a new viewer session object. - - .DESCRIPTION - This object will contain session information including active connection - objects (ClientIO) - - .PARAMETER ServerAddress - Remote Server Address. - - .PARAMETER ServerPort - Remote Server Port. - - .PARAMETER SecureString - Password used during server authentication. - - .PARAMETER TLSv1_3 - Define whether or not client must use SSL/TLS v1.3 to communicate with remote server. - Recommended if possible. - #> - + { # TODO: Check if ServerAddress is a valid host. # Or: System.Management.Automation.Runspaces.MaxPort (High(Word)) @@ -797,57 +723,296 @@ class ViewerSession $this.ServerPort = $ServerPort $this.SecurePassword = $SecurePassword $this.TLSv1_3 = $TLSv1_3 + $this.ImageCompressionQuality = $ImageCompressionQuality } [void] OpenSession() { <# .SYNOPSIS - Establish a new complete session with remote server. - - .DESCRIPTION - This method handle both session handshake and Password-Authentication. + Request a new session with remote server. #> - Write-Verbose "Open new session with remote server: ""$($this.ServerAddress):$($this.ServerPort)""..." - if ($this.SessionInformation) + Write-Verbose "Request new session with remote server: ""$($this.ServerAddress):$($this.ServerPort)""..." + + if ($this.ServerInformation) { - throw "An session already exists. Close existing session first." + throw "A session already exists." } Write-Verbose "Establish first contact with remote server..." - - $this.ClientDesktop = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) + + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) try { - $this.ClientDesktop.Connect() + $client.Connect() + + $client.Authentify($this.SecurePassword) + + Write-Verbose "Request session..." + + $client.WriteLine("REQ_SESSION") + + $this.ServerInformation = $client.ReadJson() - $this.ClientDesktop.Authentify($this.SecurePassword) + Write-Verbose "@ServerInformation:" + Write-Verbose $this.ServerInformation + Write-Verbose "---" - $this.SessionInformation = $this.ClientDesktop.Hello() + 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 - if (-not $this.SessionInformation) + (-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) { - throw "Session cannot be null." + 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-Verbose "Open secondary tunnel for input control..." + Write-Host "Remote server have " -NoNewLine + Write-Host $($this.ServerInformation.Screens.Length) -NoNewLine -ForegroundColor Green + Write-Host " different screens:`r`n" - $this.ClientEvents = [ClientIO]::new($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) - $this.ClientEvents.Connect() + 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() - $this.ClientEvents.Authentify($this.SecurePassword) + $forceResize = ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 90) - $this.ClientEvents.Hello($this.SessionInformation.SessionId) + $this.ViewerConfiguration.RequireResize = ( + $localScreenWidth -le $selectedScreen.Width -or + $localScreenHeight -le $selectedScreen.Height -or + $forceResize + ) - Write-Verbose "New session successfully established with remote server." - Write-Verbose "Session Id: $($this.SessionInformation.SessionId)" - } + if ($this.ViewerConfiguration.RequireResize -and -not $forceResize) + { + $this.ResizeRatio = 90 # Minimum default value + } + + if ($this.ViewerConfiguration.RequireResize) + { + $adjustVertically = $localScreenWidth -gt $localScreenHeight + + if ($adjustVertically) + { + $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $this.ResizeRatio) / 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 * $this.ResizeRatio) / 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 + } + + $viewerExpectation = New-Object PSCustomObject -Property @{ + ScreenName = $selectedScreen.Name + ImageCompressionQuality = $this.ImageCompressionQuality + } + + 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 + } + + Write-Verbose "@ViewerExpectation:" + Write-Verbose $viewerExpectation + Write-Verbose "---" + + $client.WriteJson($viewerExpectation) + + if ($client.ReadLine(5 * 1000) -cne "OK") + { + throw "Remote server did not respond to our expectation in time." + } + } catch { $this.CloseSession() - throw "Open Session Error. Detail: ""$($_)""" - } + throw "Could not open a new session with error: ""$($_)""" + } + finally + { + if ($client) + { + $client.Close() + } + } + } + + [ClientIO] ConnectWorker([string] $WorkerKind) + { + Write-Verbose "Connect new worker: ""$WorkerKind""..." + + $this.CheckSession() + + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) + try + { + $client.Connect() + + $client.Authentify($this.SecurePassword) + + $client.WriteLine("REQ_ATTACH") + + Write-Verbose "Attach worker to remote session ""$($this.ServerInformation.SessionId)""" + + $client.WriteLine($this.ServerInformation.SessionId) + + switch ($client.ReadLine(5 * 1000)) + { + "SESS_OK" + { + Write-Verbose "Worker successfully attached to session, define which kind of worker we are..." + + $client.WriteLine($WorkerKind) + + Write-Verbose "Worker ready." + + break + } + + "SESS_NOTFOUND" + { + throw "Server could not locate session." + } + + default + { + throw "Unexpected answer from remote server for ""REQ_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("WRKER_DESKTOP") + } + + [void] ConnectEventsWorker() + { + Write-Verbose "Create new event event (in/out) worker..." + + $this.ClientEvents = $this.ConnectWorker("WRKER_EVENT") + } + + [bool] HasSession() + { + return $this.ServerInformation -and $this.ViewerConfiguration + } + + [void] CheckSession() + { + if (-not $this.HasSession) + { + throw "Session is missing." + } } [void] CloseSession() { @@ -872,7 +1037,8 @@ class ViewerSession $this.ClientDesktop = $null $this.ClientEvents = $null - $this.SessionInformation = $null + $this.ServerInformation = $null + $this.ViewerConfiguration = $null Write-Verbose "Session closed." } @@ -880,86 +1046,20 @@ class ViewerSession } $global:VirtualDesktopUpdaterScriptBlock = { - - function Invoke-SmoothResize - { - <# - .SYNOPSIS - Output a resized version of input bitmap. The resize quality is quite fair. - - .PARAMETER OriginalImage - Input bitmap to resize. - - .PARAMETER NewWidth - Define the width of new bitmap version. - - .PARAMETER NewHeight - Define the height of new bitmap version. - - .PARAMETER HighQuality - Activate high quality image resizing with a serious performance cost. - - .EXAMPLE - Invoke-SmoothResize -OriginalImage $myImage -NewWidth 1920 -NewHeight 1024 - #> - param ( - [Parameter(Mandatory=$true)] - [System.Drawing.Bitmap] $OriginalImage, - - [Parameter(Mandatory=$true)] - [int] $NewWidth, - - [Parameter(Mandatory=$true)] - [int] $NewHeight, - - [bool] $HighQuality = $false - ) - try - { - $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $NewWidth, $NewHeight - - $resizedImage = [System.Drawing.Graphics]::FromImage($bitmap) - - if ($HighQuality) - { - $resizedImage.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality - $resizedImage.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality - $resizedImage.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic - $resizedImage.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality - } - - $resizedImage.DrawImage($OriginalImage, 0, 0, $bitmap.Width, $bitmap.Height) - - return $bitmap - } - finally - { - if ($OriginalImage) - { - $OriginalImage.Dispose() - } - - if ($resizedImage) - { - $resizedImage.Dispose() - } - } - } - try { $packetSize = 9216 # 9KiB while ($true) - { + { $stream = New-Object System.IO.MemoryStream try { - $buffer = New-Object -TypeName byte[] -ArgumentList 4 # SizeOf(Int32) + $buffer = New-Object -TypeName byte[] -ArgumentList 4 # SizeOf(Int32) - $Param.Client.SSLStream.Read($buffer, 0, $buffer.Length) - - [int32] $totalBufferSize = [BitConverter]::ToInt32($buffer, 0) + $Param.Client.SSLStream.Read($buffer, 0, $buffer.Length) + + [int32] $totalBufferSize = [BitConverter]::ToInt32($buffer, 0) $stream.SetLength($totalBufferSize) @@ -978,17 +1078,8 @@ $global:VirtualDesktopUpdaterScriptBlock = { } until ($stream.Position -eq $stream.Length) $stream.Position = 0 - - if ($Param.RequireResize) - { - $bitmap = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $stream - - $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = Invoke-SmoothResize -OriginalImage $bitmap -NewWidth $Param.VirtualDesktopWidth -NewHeight $Param.VirtualDesktopHeight - } - else - { - $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = [System.Drawing.Image]::FromStream($stream) - } + + $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = [System.Drawing.Image]::FromStream($stream) } catch { @@ -1224,6 +1315,33 @@ $global:EgressEventScriptBlock = { } } +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 { <# @@ -1364,6 +1482,17 @@ function Invoke-RemoteDesktopViewer - "Send": Send local clipboard to remote peer. - "Both": Clipboards are fully synchronized between Viewer and Server. + .PARAMETER ImageCompressionQuality + JPEG Compression level from 0 to 100 + 0 = Lowest quality. + 100 = Highest quality. + + .PARAMETER Resize + If present, apply a resize ratio parameter to remote desktop. + + .PARAMETER ResizeRatio + Define the resize ratio of remote desktop (from 30 to 90). + .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!" @@ -1372,14 +1501,25 @@ function Invoke-RemoteDesktopViewer #> param ( [string] $ServerAddress = "127.0.0.1", + + [ValidateRange(0, 65535)] [int] $ServerPort = 2801, + [switch] $TLSv1_3, [SecureString] $SecurePassword, [String] $Password, [switch] $DisableVerbosity, - [ClipboardMode] $Clipboard = [ClipboardMode]::Both + [ClipboardMode] $Clipboard = [ClipboardMode]::Both, + + [ValidateRange(0, 100)] + [int] $ImageCompressionQuality = 100, + + [switch] $Resize, + + [ValidateRange(30, 90)] + [int] $ResizeRatio = 90 ) [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() @@ -1419,12 +1559,27 @@ function Invoke-RemoteDesktopViewer Remove-Variable -Name "Password" -ErrorAction SilentlyContinue } - $session = [ViewerSession]::New($ServerAddress, $ServerPort, $SecurePassword, $TLSv1_3) + $session = [ViewerSession]::New( + $ServerAddress, + $ServerPort, + $SecurePassword, + $TLSv1_3, + $ImageCompressionQuality + ) try { + if ($Resize) + { + $session.ResizeRatio = $ResizeRatio + } + $session.OpenSession() - Write-Verbose "Create WinForms Environment..." + $session.ConnectDesktopWorker() + + $session.ConnectEventsWorker() + + Write-Verbose "Create WinForms Environment..." $virtualDesktopSyncHash = [HashTable]::Synchronized(@{ VirtualDesktop = New-VirtualDesktopForm @@ -1433,74 +1588,24 @@ function Invoke-RemoteDesktopViewer $virtualDesktopSyncHash.VirtualDesktop.Form.Text = [string]::Format( "Power Remote Desktop v{0}: {1}/{2} - {3}", $global:PowerRemoteDesktopVersion, - $session.SessionInformation.Username, - $session.SessionInformation.MachineName, - $session.SessionInformation.WindowsVersion + $session.ServerInformation.Username, + $session.ServerInformation.MachineName, + $session.ServerInformation.WindowsVersion ) - # Prepare Virtual Desktop - $locationResolutionInformation = [System.Windows.Forms.Screen]::PrimaryScreen - - $screenRect = $virtualDesktopSyncHash.VirtualDesktop.Form.RectangleToScreen($virtualDesktopSyncHash.VirtualDesktop.Form.ClientRectangle) - $captionHeight = $screenRect.Top - $virtualDesktopSyncHash.VirtualDesktop.Form.Top - - $localScreenWidth = $locationResolutionInformation.WorkingArea.Width - $localScreenHeight = $locationResolutionInformation.WorkingArea.Height - $localScreenHeight -= $captionHeight - - $requireResize = ( - ($localScreenWidth -le $session.SessionInformation.Screen.Width) -or - ($localScreenHeight -le $session.SessionInformation.Screen.Height) - ) - - $virtualDesktopWidth = 0 - $virtualDesktopHeight = 0 - - $resizeRatio = 80 - - if ($requireResize) - { - $adjustVertically = $localScreenWidth -gt $localScreenHeight - - if ($adjustVertically) - { - $virtualDesktopWidth = [math]::Round(($localScreenWidth * $resizeRatio) / 100) - - $remoteResizedRatio = [math]::Round(($virtualDesktopWidth * 100) / $session.SessionInformation.Screen.Width) - - $virtualDesktopHeight = [math]::Round(($session.SessionInformation.Screen.Height * $remoteResizedRatio) / 100) - } - else - { - $virtualDesktopHeight = [math]::Round(($localScreenHeight * $resizeRatio) / 100) - - $remoteResizedRatio = [math]::Round(($virtualDesktopHeight * 100) / $session.SessionInformation.Screen.Height) - - $virtualDesktopWidth = [math]::Round(($session.SessionInformation.Screen.Width * $remoteResizedRatio) / 100) - } - } - else - { - $virtualDesktopWidth = $session.SessionInformation.Screen.Width - $virtualDesktopHeight = $session.SessionInformation.Screen.Height - } - # Size Virtual Desktop Form Window - $virtualDesktopSyncHash.VirtualDesktop.Form.ClientSize = [System.Drawing.Size]::new($virtualDesktopWidth, $virtualDesktopHeight) + $virtualDesktopSyncHash.VirtualDesktop.Form.ClientSize = [System.Drawing.Size]::New( + $session.ViewerConfiguration.VirtualDesktopWidth, + $session.ViewerConfiguration.VirtualDesktopHeight + ) - # Center Virtual Desktop Form - $virtualDesktopSyncHash.VirtualDesktop.Form.Location = [System.Drawing.Point]::new( - (($localScreenWidth - $virtualDesktopSyncHash.VirtualDesktop.Form.Width) / 2), - (($localScreenHeight - $virtualDesktopSyncHash.VirtualDesktop.Form.Height) / 2) - ) - # 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.SessionInformation.ViewOnly) + if (-not $session.ServerInformation.ViewOnly) { enum OutputEvent { Keyboard = 0x1 @@ -1618,14 +1723,14 @@ function Invoke-RemoteDesktopViewer [string] $Button = "" ) - if ($requireResize) + if ($session.ViewerConfiguration.RequireResize) { - $X = ($X * 100) / $resizeRatio - $Y = ($Y * 100) / $resizeRatio + $X = ($X * 100) / $session.ResizeRatio + $Y = ($Y * 100) / $session.ResizeRatio } - $X += $session.SessionInformation.Screen.X - $Y += $session.SessionInformation.Screen.Y + $X += $session.ServerInformation.Screen.X + $Y += $session.ServerInformation.Screen.Y $aEvent = (New-MouseEvent -X $X -Y $Y -Button $Button -Type $Type) @@ -1690,6 +1795,16 @@ function Invoke-RemoteDesktopViewer } ) + $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.Add_KeyDown( { $result = "" @@ -1777,10 +1892,7 @@ function Invoke-RemoteDesktopViewer $param = New-Object -TypeName PSCustomObject -Property @{ Client = $session.ClientDesktop - VirtualDesktopSyncHash = $virtualDesktopSyncHash - VirtualDesktopWidth = $virtualDesktopWidth - VirtualDesktopHeight = $virtualDesktopHeight - RequireResize = $requireResize + VirtualDesktopSyncHash = $virtualDesktopSyncHash } $newRunspace = (New-RunSpace -ScriptBlock $global:VirtualDesktopUpdaterScriptBlock -Param $param) diff --git a/README.md b/README.md index 63c3533..10541db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

# PowerRemoteDesktop @@ -330,6 +330,17 @@ Detail Fingerprint * TransportMode option removed. * Desktop streaming performance / speed increased. +### XX January 2022 (2.0.0) + +* Protocol was completely revisited, protocol is now more stable and modular. +* Session concurrency is now supported, multiple viewers can connect at the same time to a server. +* Possibility to stop the server using CTRL+C +* Image quality is now requested by viewer. +* Desktop resize is now made server-side. +* Desktop resize can now be forced and requested by viewer. +* Center virtual desktop glitch fixed. +* Handshake calls (auth + session / worker negociation) will now timeout to avoid possible dead locks. + ### List of ideas and TODO * 🟢 Support Password Protected external Certificates. diff --git a/TestViewer.ps1 b/TestViewer.ps1 index 956f593..68cf9cb 100644 --- a/TestViewer.ps1 +++ b/TestViewer.ps1 @@ -2,5 +2,5 @@ Write-Output "This script is used during development phase. Never run this scrip Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Viewer\PowerRemoteDesktop_Viewer.psm1" -Raw) -Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" +Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" -Resize -ResizeRatio 50 #Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "172.31.115.183" \ No newline at end of file From e49667fad5c8c500e587e03d8b029af4df5f818b Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Tue, 25 Jan 2022 15:07:54 +0100 Subject: [PATCH 02/17] doc: redme updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10541db..23f91c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

# PowerRemoteDesktop From d0957e674005464a59c3e1273cdb9e4c9b12ed6a Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Tue, 25 Jan 2022 15:37:10 +0100 Subject: [PATCH 03/17] Feat: detect and release close dead sessions --- .../PowerRemoteDesktop_Server.psm1 | 106 ++++++++++++++++-- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 6c39eee..9997d54 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -705,8 +705,26 @@ $global:DesktopStreamScriptBlock = { $packetSize = 9216 # 9KiB + $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() + while ($true) - { + { + # Using a stopwatch instead of replacing main loop "while ($true)" by "while($this.SafeHash.SessionActive)" + # sounds strange but this is done to avoid locking our SafeHash to regularly and loosing some + # performance. If you think this is useless, just use while($this.SafeHash.SessionActive) in main + # loop instead of while($true). + if ($stopWatch.ElapsedMilliseconds -ge 2000) + { + if (-not $Param.SafeHash.SessionActive) + { + $stopWatch.Stop() + + break + } + + $stopWatch.Restart() + } + try { $desktopImage = Get-DesktopImage -Screen $screen @@ -870,7 +888,7 @@ $global:IngressEventScriptBlock = { $keyboardSim = [KeyboardSim]::New() - while ($true) + while ($Param.SafeHash.SessionActive) { try { @@ -881,8 +899,8 @@ $global:IngressEventScriptBlock = { # ($_ | Out-File "c:\temp\debug.txt") break - } - + } + try { $aEvent = $jsonEvent | ConvertFrom-Json @@ -1173,7 +1191,7 @@ $global:EgressEventScriptBlock = { $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() - while ($true) + while ($Param.SafeHash.SessionActive) { # 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 @@ -1696,6 +1714,7 @@ class ServerSession { $SafeHash = [HashTable]::Synchronized(@{ ViewerConfiguration = [ViewerConfiguration]::New() + SessionActive = $true }) ServerSession( @@ -1755,6 +1774,7 @@ class ServerSession { $param = New-Object -TypeName PSCustomObject -Property @{ Writer = $Client.Writer Clipboard = $this.Clipboard + SafeHash = $this.SafeHash } $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param)) @@ -1764,7 +1784,8 @@ class ServerSession { $param = New-Object -TypeName PSCustomObject -Property @{ Reader = $Client.Reader Clipboard = $this.Clipboard - ViewOnly = $this.ViewOnly + ViewOnly = $this.ViewOnly + SafeHash = $this.SafeHash } $this.WorkerThreads.Add((New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param)) @@ -1774,6 +1795,30 @@ class ServerSession { $this.Clients.Add($Client) } + [void] CheckSessionIntegrity() + { + <# + .SYNOPSIS + Check if session integrity is still respected. + + .DESCRIPTION + We consider that a dead session, is a session with at least one worker that has completed his + tasks. + + This will notify other workers that something happened (disconnection, fatal exception). + #> + + foreach ($worker in $this.WorkerThreads) + { + if ($worker.AsyncResult.IsCompleted) + { + $this.Close() + + break + } + } + } + [void] Close() { <# @@ -1781,6 +1826,12 @@ class ServerSession { Close components associated with current session (Ex: runspaces, sockets etc..) #> + Write-Verbose "Closing session..." + + $this.SafeHash.SessionActive = $false + + Write-Verbose "Close associated peers..." + # Close connection with remote peers associated with this session foreach ($client in $this.Clients) { @@ -1789,7 +1840,29 @@ class ServerSession { $this.Clients.Clear() - # TODO: wait for workers to finish their jobs + Write-Verbose "Wait for associated threads to finish their tasks..." + + while ($true) + { + $completed = $true + + foreach ($worker in $this.WorkerThreads) + { + if (-not $worker.AsyncResult.IsCompleted) + { + $completed = $false + + break + } + } + + if ($completed) + { break } + + Start-Sleep -Seconds 1 + } + + Write-Verbose "Dispose threads (runspaces)..." # Terminate runspaces associated with this session foreach ($worker in $this.WorkerThreads) @@ -1799,6 +1872,8 @@ class ServerSession { $worker.PowerShell.Dispose() } $this.WorkerThreads.Clear() + + Write-Verbose "Session closed." } } @@ -2093,6 +2168,15 @@ class SessionManager { throw "A server must be active to listen for new workers." } + try + { + # It is important to check regularly for dead sessions to let the garbage collector do his job + # and avoid dead threads (most of the time desktop streaming threads). + $this.CheckSessionsIntegrity() + } + catch + { } + $client = $null try { @@ -2143,6 +2227,14 @@ class SessionManager { } } + [void] CheckSessionsIntegrity() + { + foreach ($session in $this.Sessions) + { + $session.CheckSessionIntegrity() + } + } + [void] CloseServer() { <# From 2bb141047e6adaa44bf72ff0fadaa150340e03d7 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Tue, 25 Jan 2022 15:44:39 +0100 Subject: [PATCH 04/17] Fix: multi-screen mouse delta applied --- .../PowerRemoteDesktop_Viewer.psm1 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 113378f..7b87af4 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -685,7 +685,9 @@ class ViewerConfiguration { [int] $VirtualDesktopWidth = 0 [int] $VirtualDesktopHeight = 0 - [bool] $RequireResize = $false + [int] $ScreenX_Delta = 0 + [int] $ScreenY_Delta = 0 + [bool] $RequireResize = $false } class ViewerSession @@ -710,9 +712,7 @@ class ViewerSession [bool] $TLSv1_3, [int] $ImageCompressionQuality ) - { - # TODO: Check if ServerAddress is a valid host. - + { # Or: System.Management.Automation.Runspaces.MaxPort (High(Word)) if ($ServerPort -lt 0 -and $ServerPort -gt 65535) { @@ -895,6 +895,9 @@ class ViewerSession $this.ViewerConfiguration.VirtualDesktopHeight = $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 @@ -1729,8 +1732,8 @@ function Invoke-RemoteDesktopViewer $Y = ($Y * 100) / $session.ResizeRatio } - $X += $session.ServerInformation.Screen.X - $Y += $session.ServerInformation.Screen.Y + $X += $session.ViewerConfiguration.ScreenX_Delta + $Y += $session.ViewerConfiguration.ScreenY_Delta $aEvent = (New-MouseEvent -X $X -Y $Y -Button $Button -Type $Type) From 353a81f56469344c0a2728c53901501413dcc663 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Tue, 25 Jan 2022 16:22:39 +0100 Subject: [PATCH 05/17] Fix: mouse delta issue when resizing desktop --- .../PowerRemoteDesktop_Viewer.psm1 | 40 ++++++++++--------- TestViewer.ps1 | 4 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 7b87af4..1c1a16a 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -853,23 +853,15 @@ class ViewerSession $localScreenWidth = Get-LocalScreenWidth $localScreenHeight = (Get-LocalScreenHeight) - (Get-WindowCaptionHeight) - $this.ViewerConfiguration = [ViewerConfiguration]::New() - - $forceResize = ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 90) + $this.ViewerConfiguration = [ViewerConfiguration]::New() - $this.ViewerConfiguration.RequireResize = ( - $localScreenWidth -le $selectedScreen.Width -or - $localScreenHeight -le $selectedScreen.Height -or - $forceResize - ) + # TODO: Review the whole thing, something odd happends if target screen is bigger and if we force a resize ratio + if ($localScreenWidth -le $selectedScreen.Width -or $localScreenHeight -le $selectedScreen.Height) + { + $this.ViewerConfiguration.RequireResize = $true - if ($this.ViewerConfiguration.RequireResize -and -not $forceResize) - { - $this.ResizeRatio = 90 # Minimum default value - } + $this.ResizeRatio = 90 - if ($this.ViewerConfiguration.RequireResize) - { $adjustVertically = $localScreenWidth -gt $localScreenHeight if ($adjustVertically) @@ -890,9 +882,19 @@ class ViewerSession } } else - { - $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width - $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height + { + $this.ViewerConfiguration.RequireResize = ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 90) + + if ($this.ViewerConfiguration.RequireResize) + { + $this.ViewerConfiguration.VirtualDesktopWidth = ($selectedScreen.Width * $this.ResizeRatio) / 100 + $this.ViewerConfiguration.VirtualDesktopHeight = ($selectedScreen.Height * $this.ResizeRatio) / 100 + } + else + { + $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width + $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height + } } $this.ViewerConfiguration.ScreenX_Delta = $selectedScreen.X @@ -1725,12 +1727,12 @@ function Invoke-RemoteDesktopViewer [string] $Button = "" ) - + if ($session.ViewerConfiguration.RequireResize) { $X = ($X * 100) / $session.ResizeRatio $Y = ($Y * 100) / $session.ResizeRatio - } + } $X += $session.ViewerConfiguration.ScreenX_Delta $Y += $session.ViewerConfiguration.ScreenY_Delta diff --git a/TestViewer.ps1 b/TestViewer.ps1 index 68cf9cb..00a5298 100644 --- a/TestViewer.ps1 +++ b/TestViewer.ps1 @@ -2,5 +2,5 @@ Write-Output "This script is used during development phase. Never run this scrip Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Viewer\PowerRemoteDesktop_Viewer.psm1" -Raw) -Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" -Resize -ResizeRatio 50 -#Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "172.31.115.183" \ No newline at end of file +#Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" -Resize -ResizeRatio 50 +Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "172.31.115.183" -Resize -ResizeRatio 50 \ No newline at end of file From cb6ec510cfaee67ab7be303deab8e2a57350c26e Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 10:30:04 +0100 Subject: [PATCH 06/17] Opti: better mouse calculation when resizing image is required --- .../PowerRemoteDesktop_Viewer.psm1 | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 1c1a16a..c282036 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -683,11 +683,13 @@ class ClientIO { class ViewerConfiguration { + [bool] $RequireResize = $false [int] $VirtualDesktopWidth = 0 [int] $VirtualDesktopHeight = 0 [int] $ScreenX_Delta = 0 [int] $ScreenY_Delta = 0 - [bool] $RequireResize = $false + [float] $ScreenX_Ratio = 1 + [float] $ScreenY_Ratio = 1 } class ViewerSession @@ -855,18 +857,17 @@ class ViewerSession $this.ViewerConfiguration = [ViewerConfiguration]::New() - # TODO: Review the whole thing, something odd happends if target screen is bigger and if we force a resize ratio + # 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) - { - $this.ViewerConfiguration.RequireResize = $true - - $this.ResizeRatio = 90 + { + $adjustRatio = 90 $adjustVertically = $localScreenWidth -gt $localScreenHeight if ($adjustVertically) { - $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $this.ResizeRatio) / 100) + $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $adjustRatio) / 100) $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopWidth * 100) / $selectedScreen.Width) @@ -874,7 +875,7 @@ class ViewerSession } else { - $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($localScreenHeight * $this.ResizeRatio) / 100) + $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($localScreenHeight * $adjustRatio) / 100) $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopHeight * 100) / $selectedScreen.Height) @@ -882,20 +883,20 @@ class ViewerSession } } else - { - $this.ViewerConfiguration.RequireResize = ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 90) - - if ($this.ViewerConfiguration.RequireResize) - { - $this.ViewerConfiguration.VirtualDesktopWidth = ($selectedScreen.Width * $this.ResizeRatio) / 100 - $this.ViewerConfiguration.VirtualDesktopHeight = ($selectedScreen.Height * $this.ResizeRatio) / 100 - } - else - { - $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width - $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height - } + { + $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 @@ -909,7 +910,10 @@ class ViewerSession { $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 @@ -1496,7 +1500,7 @@ function Invoke-RemoteDesktopViewer If present, apply a resize ratio parameter to remote desktop. .PARAMETER ResizeRatio - Define the resize ratio of remote desktop (from 30 to 90). + Define the resize ratio of remote desktop (from 30 to 99). .EXAMPLE Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) @@ -1523,7 +1527,7 @@ function Invoke-RemoteDesktopViewer [switch] $Resize, - [ValidateRange(30, 90)] + [ValidateRange(30, 99)] [int] $ResizeRatio = 90 ) @@ -1730,8 +1734,8 @@ function Invoke-RemoteDesktopViewer if ($session.ViewerConfiguration.RequireResize) { - $X = ($X * 100) / $session.ResizeRatio - $Y = ($Y * 100) / $session.ResizeRatio + $X *= $session.ViewerConfiguration.ScreenX_Ratio + $Y *= $session.ViewerConfiguration.ScreenY_Ratio } $X += $session.ViewerConfiguration.ScreenX_Delta From 4fd7cded01940b7fd9cc5bf53596cfacd397b71c Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 11:05:11 +0100 Subject: [PATCH 07/17] Feat: virtual desktop, always on top option --- .../PowerRemoteDesktop_Viewer.psm1 | 11 +++++++++-- TestViewer.ps1 | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index c282036..fcde95f 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -1500,7 +1500,10 @@ function Invoke-RemoteDesktopViewer If present, apply a resize ratio parameter to remote desktop. .PARAMETER ResizeRatio - Define the resize ratio of remote desktop (from 30 to 99). + Define the resize ratio of remote desktop (from 30 to 99). + + .PARAMETER AlwaysOnTop + If switch is set, virtual desktop form will be above all other windows (Always on Top) .EXAMPLE Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) @@ -1528,7 +1531,9 @@ function Invoke-RemoteDesktopViewer [switch] $Resize, [ValidateRange(30, 99)] - [int] $ResizeRatio = 90 + [int] $ResizeRatio = 90, + + [switch] $AlwaysOnTop ) [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() @@ -1811,6 +1816,8 @@ function Invoke-RemoteDesktopViewer ((Get-LocalScreenWidth) - $virtualDesktopSyncHash.VirtualDesktop.Form.Width) / 2, ((Get-LocalScreenHeight) - $virtualDesktopSyncHash.VirtualDesktop.Form.Height) / 2 ) + + $virtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $AlwaysOnTop } ) diff --git a/TestViewer.ps1 b/TestViewer.ps1 index 00a5298..9bf1c7a 100644 --- a/TestViewer.ps1 +++ b/TestViewer.ps1 @@ -2,5 +2,5 @@ Write-Output "This script is used during development phase. Never run this scrip Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Viewer\PowerRemoteDesktop_Viewer.psm1" -Raw) -#Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" -Resize -ResizeRatio 50 -Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "172.31.115.183" -Resize -ResizeRatio 50 \ No newline at end of file +Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "127.0.0.1" +#Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -ServerAddress "172.31.115.183" \ No newline at end of file From 539cf5bd691bb32d7b0bf295a69202f9b3cb4364 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 11:17:22 +0100 Subject: [PATCH 08/17] Doc: readme updated --- .../PowerRemoteDesktop_Viewer.psm1 | 6 +- README.md | 85 +++++++------------ 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index fcde95f..454bde8 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -1497,13 +1497,13 @@ function Invoke-RemoteDesktopViewer 100 = Highest quality. .PARAMETER Resize - If present, apply a resize ratio parameter to remote desktop. + If this switch is present, remote desktop resize will be forced according ResizeRatio option value. .PARAMETER ResizeRatio - Define the resize ratio of remote desktop (from 30 to 99). + Define the resize ratio to apply to remote desktop (30 to 99) .PARAMETER AlwaysOnTop - If switch is set, virtual desktop form will be above all other windows (Always on Top) + If this switch is present, virtual desktop form will be above all other windows. .EXAMPLE Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) diff --git a/README.md b/README.md index 23f91c3..ea24dee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

# PowerRemoteDesktop @@ -32,30 +32,7 @@ https://user-images.githubusercontent.com/2520298/150001915-0982fb1c-a729-4b21-b * Mouse cursor icon state is synchronized between Viewer (Virtual Desktop) and Server. * Multi-Screen (Monitor) support. If remote computer have more than one desktop screen, you can choose which desktop screen to capture. * View Only mode for demonstration. You can disable remote control abilities and just show your screen to remote peer. - -## Development Roadmap - -### Version 1.x (Now marked as stable) - -Version `1.x` development is now over, only bug fix and improvements will be pushed to dedicated branch. - -### Version 2.x (In progress) - -Version `2.x` development is in progress with one new big feature and one huge improvement. - -#### Feature - -Motion detection for desktop capture. Instead of capturing the whole screen, only updated screen areas will be sent to viewer thus improving considerably the streaming speed and reducing CPU usage. - -#### Improvement - -A huge part of the protocol will be updated. - -The whole handshake progress will be cleaner and both Server and Viewer will respectively acknowledge their desired configuration. - -For example, instead of setting the image quality in server option (which makes no sense), it will be available from viewer option and sent to server. - -Same thing for image resizing, instead of resizing desktop image viewer-side, image will be resized server-side accordingly with viewer constraints. +* Session concurrency. Multiple viewers can connect to a single server at the same time. ## Installation @@ -181,16 +158,20 @@ Call `Invoke-RemoteDesktopViewer` Supported options: -* `ServerAddress` (Default: `127.0.0.1`): Remote server host/address. -* `ServerPort` (Default: `2801`): Remote server port. -* `Password` (Mandatory): Password used for server authentication. -* `DisableVerbosity` (Default: None): If this switch is present, verbosity will be hidden from console. -* `TLSv1_3` (Default: None): If this switch is present, viewer will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. -* `Clipboard` (Default: `Both`): Define clipboard synchronization rules: - * `Disabled`: Completely disable clipboard synchronization. - * `Receive`: Update local clipboard with remote clipboard only. - * `Send`: Send local clipboard to remote peer. - * `Both`: Clipboards are fully synchronized between Viewer and Server. +* **ServerAddress** (Default: `127.0.0.1`): Remote server host/address. +* **ServerPort** (Default: `2801`): Remote server port. +* **Password** (Mandatory): Password used for server authentication. +* **DisableVerbosity** (Default: None): If this switch is present, verbosity will be hidden from console. +* **TLSv1_3** (Default: None): If this switch is present, viewer will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. +* **Clipboard** (Default: `Both`): Define clipboard synchronization rules: + * **Disabled**: Completely disable clipboard synchronization. + * **Receive**: Update local clipboard with remote clipboard only. + * **Send**: Send local clipboard to remote peer. + * **Both**: Clipboards are fully synchronized between Viewer and Server. +* **ImageQuality** (Default: `100`): JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. +* **Resize** (Default: None): If this switch is present, remote desktop resize will be forced according ResizeRatio option value. +* **ResizeRatio** (Default: `90`): Define the resize ratio to apply to remote desktop (30 to 99) +* **AlwaysOnTop** (Default: None): If this switch is present, virtual desktop form will be above all other windows. #### Example @@ -204,23 +185,22 @@ Call `Invoke-RemoteDesktopServer` Supported options: -* `ListenAddress` (Default: `0.0.0.0`): Define in which interface to listen for new viewer. - * `0.0.0.0` : All interfaces - * `127.0.0.1`: Localhost interface - * `x.x.x.x`: Specific interface (Replace `x` with a valid network address) -* `ListenPort` (Default: `2801`): Define in which port to listen for new viewer. -* `Password` (**Mandatory**): Define password used during authentication process. -* `CertificateFile` (Default: **None**): A valid X509 Certificate (With Private Key) File. If set, this parameter is prioritize. -* `EncodedCertificate` (Default: **None**): A valid X509 Certificate (With Private Key) encoded as a Base64 String. -* `TLSv1_3` (Default: None): If this switch is present, server will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. -* `DisableVerbosity` (Default: None): If this switch is present, verbosity will be hidden from console. -* `ImageQuality` (Default: `100`): JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. -* `Clipboard` (Default: `Both`): Define clipboard synchronization rules: - * `Disabled`: Completely disable clipboard synchronization. - * `Receive`: Update local clipboard with remote clipboard only. - * `Send`: Send local clipboard to remote peer. - * `Both`: Clipboards are fully synchronized between Viewer and Server. -* `ViewOnly` (Default: None): If this switch is present, viewer wont be able to take the control of mouse (moves, clicks, wheel) and keyboard. Useful for view session only. +* **ListenAddress** (Default: `0.0.0.0`): Define in which interface to listen for new viewer. + * **0.0.0.0** : All interfaces + * **127.0.0.1**: Localhost interface + * **x.x.x.x**: Specific interface (Replace `x` with a valid network address) +* **ListenPort** (Default: `2801`): Define in which port to listen for new viewer. +* **Password** (**Mandatory**): Define password used during authentication process. +* **CertificateFile** (Default: **None**): A valid X509 Certificate (With Private Key) File. If set, this parameter is prioritize. +* **EncodedCertificate** (Default: **None**): A valid X509 Certificate (With Private Key) encoded as a Base64 String. +* **TLSv1_3** (Default: None): If this switch is present, server will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. +* **DisableVerbosity** (Default: None): If this switch is present, verbosity will be hidden from console. +* **Clipboard** (Default: `Both`): Define clipboard synchronization rules: + * **Disabled**: Completely disable clipboard synchronization. + * **Receive**: Update local clipboard with remote clipboard only. + * **Send**: Send local clipboard to remote peer. + * **Both**: Clipboards are fully synchronized between Viewer and Server. +* **ViewOnly** (Default: None): If this switch is present, viewer wont be able to take the control of mouse (moves, clicks, wheel) and keyboard. Useful for view session only. If no certificate option is set, then a default X509 Certificate is generated and installed on local machine (Requires Administrative Privilege) @@ -340,6 +320,7 @@ Detail Fingerprint * Desktop resize can now be forced and requested by viewer. * Center virtual desktop glitch fixed. * Handshake calls (auth + session / worker negociation) will now timeout to avoid possible dead locks. +* Virtual Desktop Form can now be set always on top of other forms. ### List of ideas and TODO From c519cd36b0ac22449845d3e5f2f2e15b09ce3eb0 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 11:41:44 +0100 Subject: [PATCH 09/17] opti: using enums as protocol verbs --- .../PowerRemoteDesktop_Server.psm1 | 39 +++++++++++++------ .../PowerRemoteDesktop_Viewer.psm1 | 37 ++++++++++++------ 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 9997d54..610e8bc 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -66,6 +66,21 @@ enum ClipboardMode { Both = 4 } +enum ProtocolCommand { + Success = 1 + Fail = 2 + RequestSession = 3 + AttachToSession = 4 + BadRequest = 5 + ResourceFound = 6 + ResourceNotFound = 7 +} + +enum WorkerKind { + Desktop = 1 + Events = 2 +} + function Write-Banner { <# @@ -1411,13 +1426,13 @@ class ClientIO { # Challenge solution is a Sha512 Hash so comparison doesn't need to be sensitive (-ceq or -cne) if ($challengeReply -ne $challengeSolution) { - $this.Writer.WriteLine("KO.") + $this.Writer.WriteLine(([ProtocolCommand]::Fail)) throw "Client challenge solution does not match our solution." } else { - $this.Writer.WriteLine("OK.") + $this.Writer.WriteLine(([ProtocolCommand]::Success)) Write-Verbose "Password Authentication Success" @@ -2098,7 +2113,7 @@ class SessionManager { $this.Sessions.Add($session) - $client.WriteLine("OK") + $client.WriteLine((([ProtocolCommand]::Success))) } catch { @@ -2130,27 +2145,27 @@ class SessionManager { $session = $this.GetSession($Client.ReadLine(5 * 1000)) if (-not $session) { - $Client.WriteLine("SESS_NOTFOUND") + $Client.WriteLine(([ProtocolCommand]::ResourceNotFound)) throw "Session object matchin given id could not be find in active session pool." } Write-Verbose "Client successfully attached to session: ""$($session.id)""" - $Client.WriteLine("SESS_OK") + $Client.WriteLine(([ProtocolCommand]::ResourceFound)) $workerKind = $Client.ReadLine(5 * 1000) - switch ($workerKind) + switch ([WorkerKind] $workerKind) { - "WRKER_DESKTOP" + (([WorkerKind]::Desktop)) { $session.NewDesktopWorker($Client) break } - "WRKER_EVENT" + (([WorkerKind]::Events)) { $session.NewEventWorker($Client) # I/O @@ -2189,16 +2204,16 @@ class SessionManager { $requestMode = $client.ReadLine(5 * 1000) - switch ($requestMode) + switch ([ProtocolCommand] $requestMode) { - "REQ_SESSION" + ([ProtocolCommand]::RequestSession) { $this.ProceedNewSessionRequest($client) break } - "REQ_ATTACH" + ([ProtocolCommand]::AttachToSession) { $this.ProceedAttachRequest($client) @@ -2207,7 +2222,7 @@ class SessionManager { default: { - $client.WriteLine("BAD_REQ") + $client.WriteLine(([ProtocolCommand]::BadRequest)) throw "Bad request." } diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 454bde8..110118e 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -72,6 +72,21 @@ enum ClipboardMode { Both = 4 } +enum ProtocolCommand { + Success = 1 + Fail = 2 + RequestSession = 3 + AttachToSession = 4 + BadRequest = 5 + ResourceFound = 6 + ResourceNotFound = 7 +} + +enum WorkerKind { + Desktop = 1 + Events = 2 +} + function Write-Banner { <# @@ -551,7 +566,7 @@ class ClientIO { $this.Writer.WriteLine($challengeSolution) $result = $this.Reader.ReadLine() - if ($result -eq "OK.") + if ($result -eq [ProtocolCommand]::Success) { Write-Verbose "Solution accepted. Authentication success." } @@ -752,7 +767,7 @@ class ViewerSession Write-Verbose "Request session..." - $client.WriteLine("REQ_SESSION") + $client.WriteLine(([ProtocolCommand]::RequestSession)) $this.ServerInformation = $client.ReadJson() @@ -921,7 +936,7 @@ class ViewerSession $client.WriteJson($viewerExpectation) - if ($client.ReadLine(5 * 1000) -cne "OK") + if ($client.ReadLine(5 * 1000) -cne [ProtocolCommand]::Success) { throw "Remote server did not respond to our expectation in time." } @@ -941,7 +956,7 @@ class ViewerSession } } - [ClientIO] ConnectWorker([string] $WorkerKind) + [ClientIO] ConnectWorker([WorkerKind] $WorkerKind) { Write-Verbose "Connect new worker: ""$WorkerKind""..." @@ -954,15 +969,15 @@ class ViewerSession $client.Authentify($this.SecurePassword) - $client.WriteLine("REQ_ATTACH") + $client.WriteLine(([ProtocolCommand]::AttachToSession)) Write-Verbose "Attach worker to remote session ""$($this.ServerInformation.SessionId)""" $client.WriteLine($this.ServerInformation.SessionId) - switch ($client.ReadLine(5 * 1000)) + switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) { - "SESS_OK" + ([ProtocolCommand]::ResourceFound) { Write-Verbose "Worker successfully attached to session, define which kind of worker we are..." @@ -973,14 +988,14 @@ class ViewerSession break } - "SESS_NOTFOUND" + ([ProtocolCommand]::ResourceNotFound) { throw "Server could not locate session." } default { - throw "Unexpected answer from remote server for ""REQ_ATTACH""." + throw "Unexpected answer from remote server for session attach." } } @@ -1001,14 +1016,14 @@ class ViewerSession { Write-Verbose "Create new desktop streaming worker..." - $this.ClientDesktop = $this.ConnectWorker("WRKER_DESKTOP") + $this.ClientDesktop = $this.ConnectWorker([WorkerKind]::Desktop) } [void] ConnectEventsWorker() { Write-Verbose "Create new event event (in/out) worker..." - $this.ClientEvents = $this.ConnectWorker("WRKER_EVENT") + $this.ClientEvents = $this.ConnectWorker([WorkerKind]::Events) } [bool] HasSession() From b9d610f1c074bbe2cc0488ab99a1232f12c0883a Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 11:44:10 +0100 Subject: [PATCH 10/17] refactor: renamed TLSv1_3 to UseTLSv1_3 --- .../PowerRemoteDesktop_Server.psm1 | 10 +++---- .../PowerRemoteDesktop_Viewer.psm1 | 26 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 610e8bc..6a620ea 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -1338,7 +1338,7 @@ class ClientIO { ClientIO( [System.Net.Sockets.TcpClient] $Client, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, - [bool] $TLSv1_3 + [bool] $UseTLSv1_3 ) { if ((-not $Client) -or (-not $Certificate)) { @@ -1351,7 +1351,7 @@ class ClientIO { $this.SSLStream = New-Object System.Net.Security.SslStream($this.Client.GetStream(), $false) - if ($TLSv1_3) + if ($UseTLSv1_3) { $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 } @@ -2328,7 +2328,7 @@ function Invoke-RemoteDesktopServer .PARAMETER EncodedCertificate A valid X509 Certificate (With Private Key) encoded as a Base64 String. - .PARAMETER TLSv1_3 + .PARAMETER UseTLSv1_3 Define whether or not TLS v1.3 must be used for communication with Viewer. .PARAMETER DisableVerbosity @@ -2358,7 +2358,7 @@ function Invoke-RemoteDesktopServer # Or [string] $EncodedCertificate = "", # 2 - [switch] $TLSv1_3, + [switch] $UseTLSv1_3, [switch] $DisableVerbosity, [ClipboardMode] $Clipboard = [ClipboardMode]::Both, [switch] $ViewOnly @@ -2433,7 +2433,7 @@ function Invoke-RemoteDesktopServer $Password, $Certificate, $ViewOnly, - $TLSv1_3, + $UseTLSv1_3, $Clipboard ) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 110118e..079aaa8 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -370,7 +370,7 @@ function Resolve-AuthenticationChallenge class ClientIO { [string] $RemoteAddress [int] $RemotePort - [bool] $TLSv1_3 + [bool] $UseTLSv1_3 [System.Net.Sockets.TcpClient] $Client = $null [System.Net.Security.SslStream] $SSLStream = $null @@ -389,16 +389,16 @@ class ClientIO { .PARAMETER RemotePort Remote server port. - .PARAMETER TLSv1_3 + .PARAMETER UseTLSv1_3 Define whether or not SSL/TLS v1.3 must be used. #> [string] $RemoteAddress = "127.0.0.1", [int] $RemotePort = 2801, - [bool] $TLSv1_3 = $false + [bool] $UseTLSv1_3 = $false ) { $this.RemoteAddress = $RemoteAddress $this.RemotePort = $RemotePort - $this.TLSv1_3 = $TLSv1_3 + $this.UseTLSv1_3 = $UseTLSv1_3 } [void]Connect() { @@ -413,7 +413,7 @@ class ClientIO { Write-Verbose "Connected." - if ($this.TLSv1_3) + if ($this.UseTLSv1_3) { $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 } @@ -715,7 +715,7 @@ class ViewerSession [string] $ServerAddress = "127.0.0.1" [string] $ServerPort = 2801 [SecureString] $SecurePassword = $null - [bool] $TLSv1_3 = $false + [bool] $UseTLSv1_3 = $false [int] $ImageCompressionQuality = 100 [int] $ResizeRatio = 0 @@ -726,7 +726,7 @@ class ViewerSession [string] $ServerAddress, [int] $ServerPort, [SecureString] $SecurePassword, - [bool] $TLSv1_3, + [bool] $UseTLSv1_3, [int] $ImageCompressionQuality ) { @@ -739,7 +739,7 @@ class ViewerSession $this.ServerAddress = $ServerAddress $this.ServerPort = $ServerPort $this.SecurePassword = $SecurePassword - $this.TLSv1_3 = $TLSv1_3 + $this.UseTLSv1_3 = $UseTLSv1_3 $this.ImageCompressionQuality = $ImageCompressionQuality } @@ -758,7 +758,7 @@ class ViewerSession Write-Verbose "Establish first contact with remote server..." - $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) try { $client.Connect() @@ -962,7 +962,7 @@ class ViewerSession $this.CheckSession() - $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) + $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) try { $client.Connect() @@ -1492,7 +1492,7 @@ function Invoke-RemoteDesktopViewer .PARAMETER Password Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) - .PARAMETER TLSv1_3 + .PARAMETER UseTLSv1_3 Define whether or not client must use SSL/TLS v1.3 to communicate with remote server. Recommended if possible. @@ -1532,7 +1532,7 @@ function Invoke-RemoteDesktopViewer [ValidateRange(0, 65535)] [int] $ServerPort = 2801, - [switch] $TLSv1_3, + [switch] $UseTLSv1_3, [SecureString] $SecurePassword, [String] $Password, @@ -1592,7 +1592,7 @@ function Invoke-RemoteDesktopViewer $ServerAddress, $ServerPort, $SecurePassword, - $TLSv1_3, + $UseTLSv1_3, $ImageCompressionQuality ) try From 74131c80958582eda2fe4272f436c44d2e5a1af9 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 12:25:32 +0100 Subject: [PATCH 11/17] feat: server now use secure string for password --- .../PowerRemoteDesktop_Server.psm1 | 107 ++++++++++++------ TestServer.ps1 | 4 +- 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 6a620ea..215d9df 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -108,6 +108,32 @@ function Write-Banner Write-Host "" } +function Get-PlainTextPassword +{ + <# + .SYNOPSIS + Retrieve the plain-text version of a secure string. + + .PARAMETER SecurePassword + The SecureString object to be reversed. + + #> + param( + [Parameter(Mandatory=$True)] + [SecureString] $SecurePassword + ) + + $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) + try + { + return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR) + } + finally + { + [Runtime.InteropServices.Marshal]::FreeBSTR($BSTR) + } +} + function Test-PasswordComplexity { <# @@ -121,17 +147,17 @@ function Test-PasswordComplexity * At least of lower case character. * At least of upper case character. - .PARAMETER PasswordCandidate - The Password to test. + .PARAMETER SecurePasswordCandidate + The password object to test #> param ( [Parameter(Mandatory=$True)] - [string] $PasswordCandidate + [SecureString] $SecurePasswordCandidate ) $complexityRules = "(?=^.{12,}$)(?=.*[!@#%^&*_]+)(?=.*[a-z])(?=.*[A-Z]).*$" - return ($PasswordCandidate -match $complexityRules) + return (Get-PlainTextPassword -SecurePassword $SecurePasswordCandidate) -match $complexityRules } function New-RandomPassword @@ -147,11 +173,15 @@ function New-RandomPassword do { $authorizedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#%^&*_" + $candidate = -join ((1..18) | ForEach-Object { Get-Random -Input $authorizedChars.ToCharArray() }) - } until (Test-PasswordComplexity -PasswordCandidate $candidate) + $secureCandidate = ConvertTo-SecureString -String $candidate -AsPlainText -Force + } until (Test-PasswordComplexity -SecurePasswordCandidate $secureCandidate) - return $candidate + $candidate = $null + + return $secureCandidate } function New-DefaultX509Certificate @@ -558,13 +588,13 @@ function Resolve-AuthenticationChallenge #> param ( [Parameter(Mandatory=$True)] - [string] $Password, + [SecureString] $SecurePassword, [Parameter(Mandatory=$True)] [string] $Candidate ) - $solution = -join($Candidate, ":", $Password) + $solution = -join($Candidate, ":", (Get-PlainTextPassword -SecurePassword $SecurePassword)) for ([int] $i = 0; $i -le 1000; $i++) { @@ -1386,7 +1416,7 @@ class ClientIO { Write-Verbose "Connection ready for use." } - [bool] Authentify([string] $Password) { + [bool] Authentify([SecureString] $SecurePassword) { <# .SYNOPSIS Handle authentication process with remote peer. @@ -1399,7 +1429,7 @@ class ClientIO { #> try { - if (-not $Password) { + if (-not $SecurePassword) { throw "During client authentication, a password cannot be blank." } @@ -1408,7 +1438,7 @@ class ClientIO { $candidate = -join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}) $candidate = Get-SHA512FromString -String $candidate - $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -Password $Password + $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -SecurePassword $SecurePassword Write-Verbose "@Challenge:" Write-Verbose "Candidate: ""${candidate}""" @@ -1591,7 +1621,7 @@ class ServerIO { } [ClientIO] PullClient( - [string] $Password, + [SecureString] $SecurePassword, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, @@ -1614,9 +1644,9 @@ class ServerIO { Other method: AsyncWaitHandle.WaitOne([timespan])'h:m:s') -eq $true|$false with BeginAcceptTcpClient(...) #> - if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) + if (-not (Test-PasswordComplexity -SecurePasswordCandidate $SecurePassword)) { - throw "Client socket pull request requires a complex password." + throw "Client socket pull request requires a complex password to be set." } if ($Timeout -gt 0) @@ -1642,7 +1672,7 @@ class ServerIO { { Write-Verbose "New client socket connected from: ""$($client.RemoteAddress())""." - $authenticated = ($client.Authentify($Password) -eq 280121) + $authenticated = ($client.Authentify($SecurePassword) -eq 280121) if (-not $authenticated) { throw "Access Denied." @@ -1898,7 +1928,7 @@ class SessionManager { [System.Collections.Generic.List[ServerSession]] $Sessions = @() - [string] $Password = "" + [SecureString] $SecurePassword = $null [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate = $null @@ -1909,7 +1939,7 @@ class SessionManager { [ClipboardMode] $Clipboard = [ClipboardMode]::Both SessionManager( - [string] $Password, + [SecureString] $SecurePassword, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, @@ -1921,12 +1951,7 @@ class SessionManager { { Write-Verbose "Initialize new session manager..." - if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) - { - throw "Session manager requires a complex password for viewer-authentication." - } - - $this.Password = $Password + $this.SecurePassword = $SecurePassword $this.ViewOnly = $ViewOnly $this.UseTLSv13 = $UseTLSv13 $this.Clipboard = $Clipboard @@ -2196,7 +2221,7 @@ class SessionManager { try { $client = $this.Server.PullClient( - $this.Password, + $this.SecurePassword, $this.Certificate, $this.UseTLSv13, 5 * 1000 @@ -2315,9 +2340,14 @@ function Invoke-RemoteDesktopServer .PARAMETER ListenPort Define in which port to listen for new viewer. + .PARAMETER SecurePassword + SecureString Password object used by remote viewer to authenticate with server (Recommended) + + Call "ConvertTo-SecureString -String "YouPasswordHere" -AsPlainText -Force" on this parameter to convert + a plain-text String to SecureString. + .PARAMETER Password - Define password used during authentication process. - (!) Absolutely use a complex password. + Plain-Text Password used by remote viewer to authenticate with server (Not recommended, use SecurePassword instead) If no password is specified, then a random complex password will be generated and printed on terminal. @@ -2352,6 +2382,7 @@ function Invoke-RemoteDesktopServer [ValidateRange(0, 65535)] [int] $ListenPort = 2801, + [SecureString] $SecurePassword, [string] $Password = "", [string] $CertificateFile = "", # 1 @@ -2407,17 +2438,23 @@ function Invoke-RemoteDesktopServer } } - if (-not $Password) + # If plain-text password is set, we convert this password to a secured representation. + if ($Password -and -not $SecurePassword) { - $Password = New-RandomPassword + $SecurePassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) + } + + if (-not $SecurePassword) + { + $SecurePassword = New-RandomPassword Write-Host -NoNewLine "Server password: """ - Write-Host -NoNewLine ${Password} -ForegroundColor green - Write-Host """." - } + Write-Host -NoNewLine $(Get-PlainTextPassword -SecurePassword $SecurePassword) -ForegroundColor green + Write-Host """." + } else { - if (-not (Test-PasswordComplexity -PasswordCandidate $Password)) + if (-not (Test-PasswordComplexity -SecurePasswordCandidate $SecurePassword)) { throw "Password complexity is too weak. Please choose a password following following rules:`r`n` * Minimum 12 Characters`r`n` @@ -2425,12 +2462,14 @@ function Invoke-RemoteDesktopServer * At least of lower case character`r`n` * At least of upper case character`r`n" } - } + } + + Remove-Variable -Name "Password" -ErrorAction SilentlyContinue try { $sessionManager = [SessionManager]::New( - $Password, + $SecurePassword, $Certificate, $ViewOnly, $UseTLSv1_3, diff --git a/TestServer.ps1 b/TestServer.ps1 index 3ef29bc..8276118 100644 --- a/TestServer.ps1 +++ b/TestServer.ps1 @@ -2,4 +2,6 @@ Write-Output "This script is used during development phase. Never run this scrip Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Server\PowerRemoteDesktop_Server.psm1" -Raw) -Invoke-RemoteDesktopServer -Password "Jade@123@Pwd" -EncodedCertificate "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" \ No newline at end of file +#Invoke-RemoteDesktopServer -Password "Jade@123@Pwd" -EncodedCertificate "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" + +Invoke-RemoteDesktopServer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -EncodedCertificate "MIIJeQIBAzCCCT8GCSqGSIb3DQEHAaCCCTAEggksMIIJKDCCA98GCSqGSIb3DQEHBqCCA9AwggPMAgEAMIIDxQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIHZPW4tq6a6ECAggAgIIDmCfxpSFuFdIf9B5cykOMvwPtM9AcpQldNOcSb1n4Ue0ehvjdE8lpoTK9Vfz1HFC7NeIgO8jSpwen7PJxQW85uXqe/7/1b6Q7eS2fJLJtNl2LMiPWEvyasJYmWW5h90Oh6T0IpD0uxWiEr0jfyDPbIWwUMmDgKi//IElxo+14z/ZGiSZuPdTiBclk1Xpsui6hnqFdPFqoZ2c4QftqormTYzeizNWbieuojuZnXoYzWjFTYKT5P4HpXylJNhLlHsJxFA88JsYcnmQg3U13eatEEmVY5DGoCtwtA7hl6CSdlnhVGhsOGh+Giri7WOJOV7cTrDcblA7EzL6yEPYvdo+yiLlPiDPOmqhC0DdCK2kwDJjcxCuiePImBke5vOJW+s9RBqKKzJQUj/2P1VTBGXgO6rWxFsh2je+7XtWtJoU1tTkH3fXH4VEiYX+is2qM+MY6WMSOLbVMzIpCJZMX4QnnR2s5mcfaovnblvv3e17Tcy/ouLEVyjRvEBnKpc3/6aACZdq83u1fryD8B2vP470lwxJEhE+aev3Pv2TBK9SdL+Xox/dWbkoc9Aqox+JK2/18tpjvKkG5ClTFdR6kfY384/WwQO4wiw0BQwCq/gG7Znkc7YKImqwobjxnD9Pq6AIyqiiAbSSA5emSbLcAo++cdbSdCwgwNksRHhzmR12nioSDq53Mqo82BltuU2PQcipBsdA00b3cD4AuVc+HIY/99gfsbKHJNLWwTX6qamUTl4dNOLCLbPIsIFrztzqGQ3cUgN5hN6fhVW5l8RLbx/s0R7pp+iO5DcLlg+kJiNBgstP+F2/bJrXKeqcron+0qJbM3o2oA5M95Za3DPVBDiW+xO0u5AxoEvk8ciQs11DS4hzOY/P7qJW1NNOa23/RMrlzt1fx3ARdSR/bGZ/jyMLdm32bT+mQ4aUCrERcKbg1vVEEeH9BG+/wKGOmBF/KJwT8e4EFFrH1Ur0Qmimhq2b2N6JaI2Fdasq4wwya3NVF3kax9vgBmO6JKfDQfy0HbRzWsCLJy8jCTUytkec0ZVmwBYEj5GubsV5knR6mqLPdIgf1gxlmmJfl9Gkzd/4bBSlt50uO10UwIO5fa1kPJXacHdzdt00RicoXycGx9HcIaY5jndV8volQmQ9WPFUTC3BL9uHQfSDxKpVn66eeATd8Ll4tp5ftakJkqXUMi9zQg7QqvvYS6dadEZcnTuCfDsBvpocutf09r4XUMIIFQQYJKoZIhvcNAQcBoIIFMgSCBS4wggUqMIIFJgYLKoZIhvcNAQwKAQKgggTuMIIE6jAcBgoqhkiG9w0BDAEDMA4ECD5W7MfyskhcAgIIAASCBMiRFTs6btm+b9m+7IcdlbVljAiIkIt+u8a8/odonMg9BXvYLw3VDkRnQ7j59S40N5S5B4L4J+FTxJW549ToOTb3gxseExRgUlX9tcb7pb6Odjp7JO+jOxRQ7f5P+AnFKVpHKs0P5z+NEp6OsANjs4h00vE5hKmAvh1N5fjGKlomps0OyIzqMCrK5jEQFGnrur4Z/3eAKH7GFKMVnWneyk/flPvjw03mcDbdY2tKlmaIKG13fqSl0gKB0Uv6lk1hLd/b7M9UC5Pqgv16Fhp0JmYC39FAtIRRZrhI8FXWDOa9TFVCS909B2jep6zIpLL1YqRY9XqYzcGLijOOr31ozFa+MGfIKoWjs0mD4B9MXtYcNy7cFJ25njbHs37+H8GUjGaUVPaR3+dkV3w/Y4z2DZRgF0XHSTFK62JqW/4ZHW6ZpnH+vdFuh+zRmV2hknfKdavxwRDYY22ebcO3YUhzVQ9gjfZHDgwp2IPb/p+Jqc6S2q+Px2MIt0H3a7uOtXm4BAANDPTS80n+nNvzp6OyaBECysjLlk1AEZtimj8+VslpHm0dm7Bl72oYh3cerBgBmFW0L2DEsU7RlnJGhva6eztNdAMngXOI2rNa2ZZdh72f3iceoTrpWCxXLggy0fN/Easm8jENSiaFKbU3wtvsIClqakSIcTD7/QF8eMQSaDy6Dgra4kwKccgl+dvMUAH9Ioeb1H3YDmnRmmm5xFtcXuL6eMj9UbLvJUoz500AMK48NVgTNJjhfkQx3KvoDZ6NwsPgK5i5VTTopw08H1iyJt+PzUDHMiHB69Zdyv6PSIGXlYw2EKt0KjxQ51bp5XgrRnGQ1uZDGRPCAZ7UsFDv09xAkOmRkOzGqbcmRoLaUmp0xJHjiCJRgbuyTPuF/6zM5Zyw3OtKfmD7+y+5oW/H0G1P3LfPrMbAtWlMv36mvKnunCQb/MtG7tyOwAP/XYvLe0LewhFnVRtvUP0ZaaL51YN6KynYHln3uK49aiwuNbidRP0HzHx6LsqaF8eVwLtXGJGoydBLZEkOUiQNUP6ohB3z3uh5HmeAhsgE+eXoL0YUG/WwtYKdqpclnOGeT17zjE1gZMAijTukERTPeFepRBkHgUXh+4T1iP7OU/Fv0jPGljYxFPjzBpfzjha4HlrBX8bg9TEMReMXvEPsZfrp4yfVT2nn2kI5mF57yUT2AyDKTXX3LaoT2Q//QltBiU1arDqfqd7I7FbvNRzB+c7bDn+nMGckfTgz0Oq4J1i2Vn9KDaQ+0GxWlxjH2HKp0/S1/AqK6dOzrK/OSXw5mxoIv7IUatt2GTfIDUwWfIAYvedMGg0IL/M0MKidOe2UviijKthogrUqLxVEb49bDnkFscZUXaSj7B+PYyQKBNtqiAf+pTZGCZwam+mVTiFPBvtMfGb8B8ZJWiTekRj6QPbw/lV+EJ6ubIAPs2rVt3z695Y8zURte6gh68wqbdBtnByIBUuU4fKRutc4EQuRYO3xe0kgNMbPMHKG4/Sy6TOVd9jI59qstcyJopZwPbUeWS6SDh2ogN3VIq9RA+GS4cmX2KrBZI1OtDCZMpBiO9Vk/08ZH/8G9bxYAfamhmL5DRazqvcnHxiYRn5B1FcNbUmlIfx5a5cAh2bDENkxJTAjBgkqhkiG9w0BCRUxFgQU6oiq2kAoZNGGRUL38qPEnlb0c7AwMTAhMAkGBSsOAwIaBQAEFFPUDCkjM9fUvXROzox5M48phvryBAhBDqmmQakSBQICCAA=" \ No newline at end of file From 411726f5e1fd3739aacbeec919bf4bc7cbe854cd Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 12:30:13 +0100 Subject: [PATCH 12/17] docs: readme updated --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ea24dee..e780d33 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,8 @@ Supported options: * **ServerAddress** (Default: `127.0.0.1`): Remote server host/address. * **ServerPort** (Default: `2801`): Remote server port. -* **Password** (Mandatory): Password used for server authentication. +* **SecurePassword**: SecureString Password object used to authenticate with remote server (Recommended) +* **Password**: Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) * **DisableVerbosity** (Default: None): If this switch is present, verbosity will be hidden from console. * **TLSv1_3** (Default: None): If this switch is present, viewer will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. * **Clipboard** (Default: `Both`): Define clipboard synchronization rules: @@ -175,7 +176,7 @@ Supported options: #### Example -`Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort 2801 -Password "Jade"` +`Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)` ### Server @@ -190,7 +191,8 @@ Supported options: * **127.0.0.1**: Localhost interface * **x.x.x.x**: Specific interface (Replace `x` with a valid network address) * **ListenPort** (Default: `2801`): Define in which port to listen for new viewer. -* **Password** (**Mandatory**): Define password used during authentication process. +* **SecurePassword**: SecureString Password object used by remote viewer to authenticate with server (Recommended) +* **Password**: Plain-Text Password used by remote viewer to authenticate with server (Not recommended, use SecurePassword instead) * **CertificateFile** (Default: **None**): A valid X509 Certificate (With Private Key) File. If set, this parameter is prioritize. * **EncodedCertificate** (Default: **None**): A valid X509 Certificate (With Private Key) encoded as a Base64 String. * **TLSv1_3** (Default: None): If this switch is present, server will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. @@ -206,9 +208,9 @@ If no certificate option is set, then a default X509 Certificate is generated an ##### Example -`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -Password "Jade"` +`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)` -`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -Password "Jade" -CertificateFile "c:\certs\phrozen.p12"` +`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12"` #### Generate and pass your own X509 Certificate @@ -321,6 +323,7 @@ Detail Fingerprint * Center virtual desktop glitch fixed. * Handshake calls (auth + session / worker negociation) will now timeout to avoid possible dead locks. * Virtual Desktop Form can now be set always on top of other forms. +* Server finally use secure string to handle password-authentication. ### List of ideas and TODO From 4bc18e81dbdc70b3fe72523b763bd1dd1c63938a Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 12:30:40 +0100 Subject: [PATCH 13/17] refactor: manifest file version updated --- .../PowerRemoteDesktop_Server.psd1 | Bin 8390 -> 8390 bytes .../PowerRemoteDesktop_Viewer.psd1 | Bin 8460 -> 8460 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 index 90dc4211cd795604a94a7c5db6face86f11b89f8..65684722a756a41afc0d1520468d72dcd991668e 100644 GIT binary patch delta 22 bcmX@+c+7D_789otgC2tc2yZTC3YG%^PBaC( delta 22 ccmX@+c+7D_789o-gC2tc5Swi-W(t-A08UB;zyJUM diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 index a61f5059cfe704d402a03bb6fc78737d0c7dae4a..bb43a2ad34c4b799dc2978588791b445861381f6 100644 GIT binary patch delta 22 bcmeBi>T%kT#l&gEpvPbU!kdekX37BoMPvn! delta 22 ccmeBi>T%kT#l&gIpvPbU#Acg|nP$oX07ZHQlmGw# From 2085992e199cdcc54772bac1342563a90dc2443e Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 12:47:06 +0100 Subject: [PATCH 14/17] feat: display to terminal important events without verbosity --- .../PowerRemoteDesktop_Server.psm1 | 23 +++++++++++++++---- .../PowerRemoteDesktop_Viewer.psm1 | 14 +++++++---- TestServer.ps1 | 2 -- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 215d9df..d2a6c1a 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -1750,6 +1750,7 @@ class ServerSession { [string] $Id = "" [bool] $ViewOnly = $false [ClipboardMode] $Clipboard = [ClipboardMode]::Both + [string] $ViewerLocation = "" [System.Collections.Generic.List[PSCustomObject]] $WorkerThreads = @() @@ -1764,13 +1765,15 @@ class ServerSession { ServerSession( [bool] $ViewOnly, - [ClipboardMode] $Clipboard + [ClipboardMode] $Clipboard, + [string] $ViewerLocation ) { $this.Id = (SHA512FromString -String (-join ((1..128) | ForEach-Object {Get-Random -input ([char[]](33..126))}))) $this.ViewOnly = $ViewOnly $this.Clipboard = $Clipboard + $this.ViewerLocation = $ViewerLocation } [bool] CompareSession([string] $Id) @@ -1877,7 +1880,7 @@ class ServerSession { Write-Verbose "Close associated peers..." - # Close connection with remote peers associated with this session + # Close connection with remote peers associated with this session foreach ($client in $this.Clients) { $client.Close() @@ -1918,6 +1921,8 @@ class ServerSession { } $this.WorkerThreads.Clear() + Write-Host "Session terminated with viewer: $($this.ViewerLocation)" + Write-Verbose "Session closed." } } @@ -2072,7 +2077,7 @@ class SessionManager { { Write-Verbose "Remote peer as requested a new session..." - $session = [ServerSession]::New($this.ViewOnly, $this.Clipboard) + $session = [ServerSession]::New($this.ViewOnly, $this.Clipboard, $client.RemoteAddress()) Write-Verbose "@ServerSession" Write-Verbose "Id: ""$($session.Id)""" @@ -2232,9 +2237,13 @@ class SessionManager { switch ([ProtocolCommand] $requestMode) { ([ProtocolCommand]::RequestSession) - { + { + $remoteAddress = $client.RemoteAddress() + $this.ProceedNewSessionRequest($client) + Write-Host "New remote desktop session established with: $($remoteAddress)" + break } @@ -2468,6 +2477,8 @@ function Invoke-RemoteDesktopServer try { + Write-Host "Loading remote desktop server components..." + $sessionManager = [SessionManager]::New( $SecurePassword, $Certificate, @@ -2480,6 +2491,8 @@ function Invoke-RemoteDesktopServer $ListenAddress, $ListenPort ) + + Write-Host "Server is ready to receive new connections..." $sessionManager.ListenForWorkers() } @@ -2491,6 +2504,8 @@ function Invoke-RemoteDesktopServer $sessionManager = $null } + + Write-Host "Remote desktop was closed." } } finally diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 079aaa8..d67f647 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -1566,9 +1566,7 @@ function Invoke-RemoteDesktopViewer else { $VerbosePreference = "SilentlyContinue" - } - - $VerbosePreference = "continue" + } Write-Banner @@ -1602,12 +1600,16 @@ function Invoke-RemoteDesktopViewer $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(@{ @@ -1956,7 +1958,7 @@ function Invoke-RemoteDesktopViewer $null = $virtualDesktopSyncHash.VirtualDesktop.Form.ShowDialog() } finally - { + { Write-Verbose "Free environement." if ($session) @@ -1979,7 +1981,9 @@ function Invoke-RemoteDesktopViewer if ($virtualDesktopSyncHash.VirtualDesktop) { $virtualDesktopSyncHash.VirtualDesktop.Form.Dispose() - } + } + + Write-Host "Remote desktop session has ended." } } finally diff --git a/TestServer.ps1 b/TestServer.ps1 index 8276118..c143316 100644 --- a/TestServer.ps1 +++ b/TestServer.ps1 @@ -2,6 +2,4 @@ Write-Output "This script is used during development phase. Never run this scrip Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Server\PowerRemoteDesktop_Server.psm1" -Raw) -#Invoke-RemoteDesktopServer -Password "Jade@123@Pwd" -EncodedCertificate "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" - Invoke-RemoteDesktopServer -SecurePassword (ConvertTo-SecureString -String "Jade@123@Pwd" -AsPlainText -Force) -EncodedCertificate "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" \ No newline at end of file From abd85bded21aa79b97b7ba9d276601173e376d6c Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 18:19:32 +0100 Subject: [PATCH 15/17] docs: readme updated --- README.md | 308 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 217 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index e780d33..4bf72f5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-# PowerRemoteDesktop +# Power Remote Desktop @@ -14,12 +14,14 @@ This project demonstrate why PowerShell contains the word *Power*. It is unfortu Tested on: -* Windows 10 - PowerShell Version: 5.1.19041.1320 -* Windows 11 - PowerShell Version: 5.1.22000.282 +* **Windows 10** - PowerShell Version: 5.1.19041.1320 +* **Windows 11** - PowerShell Version: 5.1.22000.282 -Current version: **1.0.6 Stable** +Current version: **2.0 Stable** -## Features +--- + +## Highlighted Features https://user-images.githubusercontent.com/2520298/150001915-0982fb1c-a729-4b21-b22c-a58e201bfe27.mp4 @@ -34,19 +36,43 @@ https://user-images.githubusercontent.com/2520298/150001915-0982fb1c-a729-4b21-b * View Only mode for demonstration. You can disable remote control abilities and just show your screen to remote peer. * Session concurrency. Multiple viewers can connect to a single server at the same time. -## Installation +--- + +## Setup everything in less than a minute (Fast Setup) + +````powershell +Install-Module -Name PowerRemoteDesktop_Server + +Invoke-RemoteDesktopServer -CertificateFile "" +```` + +If you are even more lazy and want to avoid using your own certificate, remove `CertificateFile` option but you will need to run PowerShell as Administrator. + +````powershell +Install-Module -Name PowerRemoteDesktop_Viewer + +Invoke-RemoteDesktopServer -ServerAddress "" -Password "" +```` + +Thats it 😉 + +--- + +## Detailed Installation and Instructions You will find multiple ways to use this PowerShell Applications. Recommended method would be to install both Server and Viewer using the PowerShell Gallery but you can also do it manually as an installed module or imported script. -### Install as a PowerShell Module from PowerShell Gallery (Recommended) +### Install as a PowerShell Module from PowerShell Gallery (**Recommended**) You can install Power Remote Desktop from PowerShell Gallery. See PowerShell Gallery as the 'equivalent' of Aptitude for Debian or Brew for MacOS. Run the following commands: -`Install-Module -Name PowerRemoteDesktop_Server` +```powershell +Install-Module -Name PowerRemoteDesktop_Server -`Install-Module -Name PowerRemoteDesktop_Viewer` +Install-Module -Name PowerRemoteDesktop_Viewer +``` `AllowPrerelease` is mandatory when current version is marked as a *Prerelease* @@ -63,7 +89,9 @@ Answer `Y` to proceed installation. Both modules should now be available, you can verify using the command: -`Get-Module -ListAvailable` +```powershell +Get-Module -ListAvailable +``` Example Output: @@ -84,9 +112,11 @@ Manifest 1.0.0 PowerRemoteDesktop_Viewer Invoke-RemoteDesktopVi If you don't see them, run the following commands and check back. -`Import-Module PowerRemoteDesktop_Server` +```powershell +Import-Module PowerRemoteDesktop_Server -`Import-Module PowerRemoteDesktop_Viewer` +Import-Module PowerRemoteDesktop_Viewer + ``` ### Install as a PowerShell Module (Manually / Unmanaged) @@ -94,7 +124,9 @@ To be available, the module must first be present in a registered module path. You can list module paths with following command: -`Write-Output $env:PSModulePath` +```powershell +Write-Output $env:PSModulePath +``` Example Output: @@ -104,13 +136,23 @@ C:\Users\Phrozen\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPow Clone PowerRemoteDesktop repository or download a Github release package. -`git clone https://github.com/DarkCoderSc/PowerRemoteDesktop.git` +``` +git clone https://github.com/DarkCoderSc/PowerRemoteDesktop.git +``` + +Copy both *PowerRemoteDesktop_Viewer* and *PowerRemoteDesktop_Server* folders to desired module path -Copy both *PowerRemoteDesktop_Viewer* and *PowerRemoteDesktop_Server* folders to desired module path (Ex: `C:\Users\\Documents\WindowsPowerShell\Modules`) +Example: + +``` +C:\Users\\Documents\WindowsPowerShell\Modules +``` Both modules should now be available, you can verify using the command: -`Get-Module -ListAvailable` +```powershell +Get-Module -ListAvailable +``` Example Output: @@ -131,9 +173,11 @@ Manifest 1.0.0 PowerRemoteDesktop_Viewer Invoke-RemoteDesktopVi If you don't see them, run the following commands and check back. -`Import-Module PowerRemoteDesktop_Server` +```powershell +Import-Module PowerRemoteDesktop_Server -`Import-Module PowerRemoteDesktop_Viewer` +Import-Module PowerRemoteDesktop_Viewer +``` Notice: Manifest files are optional (`*.psd1`) and can be removed. @@ -143,8 +187,17 @@ It is not mandatory to install this application as a PowerShell module (Even if You can also load it as a PowerShell Script. Multiple methods exists including: -* Invoking Commands Using: `IEX (Get-Content .\PowerRemoteDesktop_Viewer.psm1 -Raw)` and `IEX (Get-Content .\PowerRemoteDesktop_[Server/Viewer].psm1 -Raw)` -* Loading script from a remote location: `IEX (New-Object Net.WebClient).DownloadString('http://127.0.0.1/PowerRemoteDesktop_[Server/Viewer].psm1)` +Invoking Commands Using: + +```powershell +IEX (Get-Content .\PowerRemoteDesktop_[Server/Viewer].psm1 -Raw) +``` + +Loading script from a remote location: + +```powershell +IEX (New-Object Net.WebClient).DownloadString('http://127.0.0.1/PowerRemoteDesktop_[Server/Viewer].psm1') +``` etc... @@ -152,115 +205,186 @@ etc... ### Client -`PowerRemoteDesktop_Viewer.psm1` module first need to be imported / installed on current PowerShell session. +`PowerRemoteDesktop_Viewer.psm1` needs to be imported / or installed on local machine. + +#### Available Module Functions + +```powershell +Invoke-RemoteDesktopViewer +Get-TrustedServers +Remove-TrustedServer +Clear-TrustedServers +``` + +#### Invoke-RemoteDesktopViewer + +Create a new remote desktop session with a Power Remote Desktop Server. + +##### ⚙️ Supported Options: -Call `Invoke-RemoteDesktopViewer` +| Parameter | Type | Default | Description | +|-------------------|------------------|------------|--------------| +| ServerAddress | String | 127.0.0.1 | Remote server host/address | +| ServerPort | Integer | 2801 | Remote server port | +| SecurePassword | SecureString | None | SecureString object containing password used to authenticate with remote server (Recommended) | +| Password | String | None | Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) | +| DisableVerbosity | Switch | False | If present, program wont show verbosity messages | +| UseTLSv1_3 | Switch | False | If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) | +| Clipboard | Enum | Both | Define clipboard synchronization mode (`Both`, `Disabled`, `Send`, `Receive`) see bellow for more detail | +| ImageQuality | Integer (0-100) | 100 | JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. | +| Resize | Switch | False | If present, remote desktop will get resized accordingly with `ResizeRatio` option. | +| ResizeRatio | Integer (30-99) | 90 | Used with `Resize` option, define the resize ratio in percentage. | +| AlwaysOnTop | Switch | False | If present, virtual desktop form will be above all other window's | -Supported options: +##### Clipboard Mode Enum Properties -* **ServerAddress** (Default: `127.0.0.1`): Remote server host/address. -* **ServerPort** (Default: `2801`): Remote server port. -* **SecurePassword**: SecureString Password object used to authenticate with remote server (Recommended) -* **Password**: Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) -* **DisableVerbosity** (Default: None): If this switch is present, verbosity will be hidden from console. -* **TLSv1_3** (Default: None): If this switch is present, viewer will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. -* **Clipboard** (Default: `Both`): Define clipboard synchronization rules: - * **Disabled**: Completely disable clipboard synchronization. - * **Receive**: Update local clipboard with remote clipboard only. - * **Send**: Send local clipboard to remote peer. - * **Both**: Clipboards are fully synchronized between Viewer and Server. -* **ImageQuality** (Default: `100`): JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. -* **Resize** (Default: None): If this switch is present, remote desktop resize will be forced according ResizeRatio option value. -* **ResizeRatio** (Default: `90`): Define the resize ratio to apply to remote desktop (30 to 99) -* **AlwaysOnTop** (Default: None): If this switch is present, virtual desktop form will be above all other windows. +| Value | Description | +|-------------------|------------------| +| 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 | + +##### ⚠️ Important Notices + +Prefer using `SecurePassword` over plain-text password even if a plain-text password is getting converted to `SecureString` anyway. #### Example -`Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)` +Open a new remote desktop session to `127.0.0.1:2801` with password `urCompl3xP@ssw0rd` -### Server +```powershell +Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) +``` -`PowerRemoteDesktop_Server.psm1` module first need to be imported / installed on current PowerShell session. +#### Enumerate Trusted Servers -Call `Invoke-RemoteDesktopServer` +When a fingerprint is met for the first time, viewer will ask you if you want to trust this new remote server fingerprint. -Supported options: +When you choose the `[A] Always` option, this fingerprint will be saved to local user registry. If you change your mind, you can revoke trsuted fingerprint at any time using dedicated functions. -* **ListenAddress** (Default: `0.0.0.0`): Define in which interface to listen for new viewer. - * **0.0.0.0** : All interfaces - * **127.0.0.1**: Localhost interface - * **x.x.x.x**: Specific interface (Replace `x` with a valid network address) -* **ListenPort** (Default: `2801`): Define in which port to listen for new viewer. -* **SecurePassword**: SecureString Password object used by remote viewer to authenticate with server (Recommended) -* **Password**: Plain-Text Password used by remote viewer to authenticate with server (Not recommended, use SecurePassword instead) -* **CertificateFile** (Default: **None**): A valid X509 Certificate (With Private Key) File. If set, this parameter is prioritize. -* **EncodedCertificate** (Default: **None**): A valid X509 Certificate (With Private Key) encoded as a Base64 String. -* **TLSv1_3** (Default: None): If this switch is present, server will use TLS v1.3 instead of TLS v1.2. Use this option only if both viewer and server support TLS v1.3. -* **DisableVerbosity** (Default: None): If this switch is present, verbosity will be hidden from console. -* **Clipboard** (Default: `Both`): Define clipboard synchronization rules: - * **Disabled**: Completely disable clipboard synchronization. - * **Receive**: Update local clipboard with remote clipboard only. - * **Send**: Send local clipboard to remote peer. - * **Both**: Clipboards are fully synchronized between Viewer and Server. -* **ViewOnly** (Default: None): If this switch is present, viewer wont be able to take the control of mouse (moves, clicks, wheel) and keyboard. Useful for view session only. +```powershell +Get-TrustedServers +``` -If no certificate option is set, then a default X509 Certificate is generated and installed on local machine (Requires Administrative Privilege) +Example output: -##### Example +```` +PS C:\Users\Phrozen\Desktop\Projects\PowerRemoteDesktop> Get-TrustedServers -`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)` +Detail Fingerprint +------ ----------- +@{FirstSeen=18/01/2022 19:40:24} D9F4637463445D6BB9F3EFBF08E06BE4C27035AF +@{FirstSeen=20/01/2022 15:52:33} 3FCBBFB37CF6A9C225F7F582F14AC4A4181BED53 +@{FirstSeen=20/01/2022 16:32:14} EA88AADA402864D1864542F7F2A3C49E56F473B0 +@{FirstSeen=21/01/2022 12:24:18} 3441CE337A59FC827466FC954F2530C76A3F8FE4 +```` -`Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12"` +### Permanently Delete a Trusted Server -#### Generate and pass your own X509 Certificate +```powershell +Remove-TrustedServer -Fingerprint "" +``` -Passing your own X509 Certificate is very useful if you want to avoid running your PowerShell Instance as Administrator. +### Permanently Delete all Trusted Servers (Purge) -You can easily create your own X509 Certificate using OpenSSL Command Line Tool. +```powershell +Clear-TrustedServers +``` -##### Generate your Certificate +--- -`openssl req -x509 -sha512 -nodes -days 365 -newkey rsa:4096 -keyout phrozen.key -out phrozen.crt` +### Server -Then export the new certificate with Private Key Included. +`PowerRemoteDesktop_Server.psm1` needs to be imported / or installed on local machine. -`openssl pkcs12 -export -out phrozen.p12 -inkey phrozen.key -in phrozen.crt` +#### Available Module Functions -##### Use it as file +```powershell +Invoke-RemoteDesktopServer +``` -Pass the certificate file to parameter `CertificateFile`. +##### ⚙️ Supported Options: + +| Parameter | Type | Default | Description | +|--------------------|------------------|------------|--------------| +| ServerAddress | String | 0.0.0.0 | IP Address that represents the local IP address | +| ServerPort | Integer | 2801 | The port on which to listen for incoming connection | +| SecurePassword | SecureString | None | SecureString object containing password used to authenticate remote viewer (Recommended) | +| Password | String | None | Plain-Text Password used to authenticate remote viewer (Not recommended, use SecurePassword instead) | +| DisableVerbosity | Switch | False | If present, program wont show verbosity messages | +| UseTLSv1_3 | Switch | False | If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) | +| Clipboard | Enum | Both | Define clipboard synchronization mode (`Both`, `Disabled`, `Send`, `Receive`) see bellow for more detail | +| CertificateFile | String | None | A file containing valid certificate information (x509), must include the **private key** | +| EncodedCertificate | String | None | A **base64** representation of the whole certificate file, must include the **private key** | +| ViewOnly | Switch | False | If present, remote viewer is only allowed to view the desktop (Mouse and Keyboard are not authorized) | + +##### Server Address Examples + +| Value | Description | +|-------------------|------------------| +| 127.0.0.1 | Only listen for localhost connection (most likely for debugging purpose) | +| 0.0.0.0 | Listen on all network interfaces (Local, LAN, WAN) | + +##### Clipboard Mode Enum Properties + +| Value | Description | +|-------------------|------------------| +| 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 | + +##### ⚠️ Important Notices + +1. Prefer using `SecurePassword` over plain-text password even if a plain-text password is getting converted to `SecureString` anyway. +2. Not specifying a custom certificate using `CertificateFile` or `EncodedCertificate` result in generating a default self-signed certificate (if not already generated) that will get installed on local machine thus requiring administrator privilege. If you want to run the server as a non-privileged account, specify your own certificate location. +3. If you don't specify a `SecurePassword` or `Password`, a random complex password will be generated and displayed on terminal (this password is temporary). + +##### Examples + +```powershell +Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) + +Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12" +``` -##### Use it as Encoded Base64 String +#### Generate and pass your own X509 Certificate -First encode your certificate file as base64 string. +⚠️ Remember that not using your own X509 certificate will result in requiring administrator privilege to create a new server. -`base64 -i phrozen.p12` +Fortunately, you can easily create your own X509 certificate for example with the help of [OpenSSL command line tool](https://www.openssl.org). -Then pass the encoded string to parameter `EncodedCertificate`. +##### Generate your Certificate -### List trusted servers +``` +openssl req -x509 -sha512 -nodes -days 365 -newkey rsa:4096 -keyout phrozen.key -out phrozen.crt +``` -It is now possible to persistantly trust server using a local storage (Windows User Registry Hive) +Then export the new certificate (**must include private key**). -`Get-TrustedServers` +``` +openssl pkcs12 -export -out phrozen.p12 -inkey phrozen.key -in phrozen.crt +``` -Example output: +##### Integrate to server as a file -```` -PS C:\Users\Phrozen\Desktop\Projects\PowerRemoteDesktop> Get-TrustedServers +Use `CertificateFile`. Example: `c:\tlscert\phrozen.crt` -Detail Fingerprint ------- ----------- -@{FirstSeen=14/01/2022 11:06:16} EA88AADA402864D1864542F7F2A3C49E56F473B0 -```` +##### Integrate to server as a base64 representation -### Delete trusted server (Permanently) +Encode an existing certificate using PowerShell -`Remove-TrustedServer` +```powershell +[convert]::ToBase64String((Get-Content -path "c:\tlscert\phrozen.crt" -Encoding byte)) +``` +or on Linux / Mac systems -### Delete all trusted servers (Permanently) +``` +base64 -i /tmp/phrozen.p12 +``` -`Clear-TrustedServers` +You can then pass the output base64 certificate file to parameter `EncodedCertificate` (One line) ## Changelog @@ -353,4 +477,6 @@ Jean-Pierre LESUEUR. For these external sites, PHROZEN SASU and / or Jean-Pierre 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. +--- +Made with ❤️ in 🇫🇷 From 5e980390427d9472f715b232d1e72e6fb2feeb1d Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 18:20:29 +0100 Subject: [PATCH 16/17] docs: readme updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4bf72f5..cb46b3e 100644 --- a/README.md +++ b/README.md @@ -477,6 +477,7 @@ Jean-Pierre LESUEUR. For these external sites, PHROZEN SASU and / or Jean-Pierre 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. + --- Made with ❤️ in 🇫🇷 From 6f75e6ed5a69981ff7a57a36ee7eec73d9446000 Mon Sep 17 00:00:00 2001 From: Jean-Pierre LESUEUR Date: Wed, 26 Jan 2022 18:45:56 +0100 Subject: [PATCH 17/17] docs: readme updated --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cb46b3e..c653c05 100644 --- a/README.md +++ b/README.md @@ -222,19 +222,19 @@ Create a new remote desktop session with a Power Remote Desktop Server. ##### ⚙️ Supported Options: -| Parameter | Type | Default | Description | -|-------------------|------------------|------------|--------------| -| ServerAddress | String | 127.0.0.1 | Remote server host/address | -| ServerPort | Integer | 2801 | Remote server port | -| SecurePassword | SecureString | None | SecureString object containing password used to authenticate with remote server (Recommended) | -| Password | String | None | Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) | -| DisableVerbosity | Switch | False | If present, program wont show verbosity messages | -| UseTLSv1_3 | Switch | False | If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) | -| Clipboard | Enum | Both | Define clipboard synchronization mode (`Both`, `Disabled`, `Send`, `Receive`) see bellow for more detail | -| ImageQuality | Integer (0-100) | 100 | JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. | -| Resize | Switch | False | If present, remote desktop will get resized accordingly with `ResizeRatio` option. | -| ResizeRatio | Integer (30-99) | 90 | Used with `Resize` option, define the resize ratio in percentage. | -| AlwaysOnTop | Switch | False | If present, virtual desktop form will be above all other window's | +| Parameter | Type | Default | Description | +|-------------------------|------------------|------------|--------------| +| ServerAddress | String | 127.0.0.1 | Remote server host/address | +| ServerPort | Integer | 2801 | Remote server port | +| SecurePassword | SecureString | None | SecureString object containing password used to authenticate with remote server (Recommended) | +| Password | String | None | Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) | +| DisableVerbosity | Switch | False | If present, program wont show verbosity messages | +| UseTLSv1_3 | Switch | False | If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) | +| Clipboard | Enum | Both | Define clipboard synchronization mode (`Both`, `Disabled`, `Send`, `Receive`) see bellow for more detail | +| ImageCompressionQuality | Integer (0-100) | 100 | JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. | +| Resize | Switch | False | If present, remote desktop will get resized accordingly with `ResizeRatio` option. | +| ResizeRatio | Integer (30-99) | 90 | Used with `Resize` option, define the resize ratio in percentage. | +| AlwaysOnTop | Switch | False | If present, virtual desktop form will be above all other window's | ##### Clipboard Mode Enum Properties @@ -436,7 +436,7 @@ You can then pass the output base64 certificate file to parameter `EncodedCertif * TransportMode option removed. * Desktop streaming performance / speed increased. -### XX January 2022 (2.0.0) +### 28 January 2022 (2.0.0) * Protocol was completely revisited, protocol is now more stable and modular. * Session concurrency is now supported, multiple viewers can connect at the same time to a server.