diff --git a/Assets/multi-screen.png b/Assets/multi-screen.png new file mode 100644 index 0000000..86c3b0f Binary files /dev/null and b/Assets/multi-screen.png differ diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 index f40171e..0f6b9fa 100644 Binary files a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 and b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 differ diff --git a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 index 5ed5501..be19dde 100644 --- a/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 +++ b/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psm1 @@ -51,10 +51,9 @@ Add-Type -Assembly System.Windows.Forms Add-Type -Assembly System.Drawing -Add-Type -MemberDefinition '[DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);' -Name GDI32 -Namespace W; -Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern int GetDC(IntPtr hWnd);[DllImport("User32.dll")] public static extern int ReleaseDC(IntPtr hwnd, int hdc);[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W; +Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W; -$global:PowerRemoteDesktopVersion = "1.0.beta.3" +$global:PowerRemoteDesktopVersion = "1.0.3.beta.4" enum TransportMode { Raw = 1 @@ -97,7 +96,7 @@ function Test-PasswordComplexity .DESCRIPTION To return True, Password must follow bellow complexity rules: * Minimum 12 Characters. - * One of following symbols: "!@#$%^&*_". + * One of following symbols: "!@#%^&*_". * At least of lower case character. * At least of upper case character. @@ -109,7 +108,7 @@ function Test-PasswordComplexity [string] $PasswordCandidate ) - $complexityRules = "(?=^.{12,}$)(?=.*[!@#$%^&*_]+)(?=.*[a-z])(?=.*[A-Z]).*$" + $complexityRules = "(?=^.{12,}$)(?=.*[!@#%^&*_]+)(?=.*[a-z])(?=.*[A-Z]).*$" return ($PasswordCandidate -match $complexityRules) } @@ -501,24 +500,6 @@ function Resolve-AuthenticationChallenge return $solution } -function Get-ResolutionScaleFactor -{ - <# - .SYNOPSIS - Return current screen scale factor - #> - - $hdc = [W.User32]::GetDC(0) - try - { - return [W.GDI32]::GetDeviceCaps($hdc, 117) / [W.GDI32]::GetDeviceCaps($hdc, 10) - } - finally - { - [W.User32]::ReleaseDC(0, $hdc) | Out-Null - } -} - function Get-LocalMachineInformation { <# @@ -531,19 +512,30 @@ function Get-LocalMachineInformation This function is expected to be progressively updated with new required session information. #> - $screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + + $screens = @() + + $i = 0 + foreach ($screen in ([System.Windows.Forms.Screen]::AllScreens | Sort-Object -Property Primary -Descending)) + { + $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 - - ScreenInformation = New-Object -TypeName PSCustomObject -Property @{ - Width = $screenBounds.Width - Height = $screenBounds.Height - X = $screenBounds.X - Y = $screenBounds.Y - } + Screens = ($screens) } } @@ -551,10 +543,11 @@ class ServerSession { <# .SYNOPSIS PowerRemoteDesktop Session Class. - #> + #> [string] $Id = "" [string] $TiedAddress = "" + [string] $Screen = "" ServerSession([string] $RemoteAddress) { <# @@ -567,11 +560,10 @@ class ServerSession { #> $this.Id = (SHA512FromString -String (-join ((33..126) | Get-Random -Count 128 | ForEach-Object{[char] $_}))) - $this.TiedAddress = $RemoteAddress + $this.TiedAddress = $RemoteAddress } - [bool] CompareWith([string] $Id, [string] $RemoteAddress) - { + [bool] CompareWith([string] $Id, [string] $RemoteAddress) { return ($this.Id -eq $Id) -and ($this.TiedAddress -eq $RemoteAddress) } } @@ -777,6 +769,15 @@ class ClientIO { $this.Writer.WriteLine(($sessionInformation | ConvertTo-Json -Compress)) + 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." + + $screenName = $this.Reader.ReadLine() + + $session.Screen = $screenName + } + Write-Verbose "Handshake done." return $session @@ -1043,24 +1044,28 @@ $global:DesktopStreamScriptBlock = { .SYNOPSIS Return a snapshot of primary screen desktop. - .DESCRIPTION - Notice: - At this time, PowerRemoteDesktop only supports PrimaryScreen. - Even if multi-screen capture is a very easy feature to implement, It will probably be present - in final version 1.0 + .PARAMETER Screen + Define target screen to capture (if multiple monitor exists). + Default is primary screen #> + param ( + [System.Windows.Forms.Screen] $Screen = $null + ) try { - $primaryDesktop = [System.Windows.Forms.Screen]::PrimaryScreen + if (-not $Screen) + { + $Screen = [System.Windows.Forms.Screen]::PrimaryScreen + } $size = New-Object System.Drawing.Size( - $primaryDesktop.Bounds.Size.Width, - $primaryDesktop.Bounds.Size.Height + $Screen.Bounds.Size.Width, + $Screen.Bounds.Size.Height ) $location = New-Object System.Drawing.Point( - $primaryDesktop.Bounds.Location.X, - $primaryDesktop.Bounds.Location.Y + $Screen.Bounds.Location.X, + $Screen.Bounds.Location.Y ) $bitmap = New-Object System.Drawing.Bitmap($size.Width, $size.Height) @@ -1087,6 +1092,10 @@ $global:DesktopStreamScriptBlock = { } $imageQuality = 100 + if ($syncHash.Param.ImageQuality -ge 0 -and $syncHash.Param.ImageQuality -lt 100) + { + $imageQuality = $syncHash.Param.ImageQuality + } try { [System.IO.MemoryStream] $oldImageStream = New-Object System.IO.MemoryStream @@ -1102,7 +1111,7 @@ $global:DesktopStreamScriptBlock = { { try { - $desktopImage = Get-DesktopImage + $desktopImage = Get-DesktopImage -Screen $syncHash.Param.Screen $imageStream = New-Object System.IO.MemoryStream @@ -1509,6 +1518,11 @@ function Invoke-RemoteDesktopServer .PARAMETER DisableVerbosity Disable verbosity (not recommended) + + .PARAMETER ImageQuality + JPEG Compression level from 0 to 100 + 0 = Lowest quality. + 100 = Highest quality. #> param ( @@ -1523,7 +1537,9 @@ function Invoke-RemoteDesktopServer [TransportMode] $TransportMode = "Raw", [switch] $TLSv1_3, - [switch] $DisableVerbosity + [switch] $DisableVerbosity, + + [int] $ImageQuality = 100 ) @@ -1546,10 +1562,7 @@ function Invoke-RemoteDesktopServer Write-Banner - if (Get-ResolutionScaleFactor -ne 1) - { - [W.User32]::SetProcessDPIAware() - } + [W.User32]::SetProcessDPIAware() | Out-Null if (-not (Test-Administrator) -and -not $CertificateFile -and -not $EncodedCertificate) { @@ -1572,8 +1585,8 @@ function Invoke-RemoteDesktopServer if (-not $Password) { $Password = ( - # a-Z, 0-9, !@#$%^&*_ - -join ((48..57) + (64..90) + (35..38) + 33 + 42 + 94 + 95 + (97..122) | Get-Random -Count 18 | ForEach-Object{[char] $_}) + # a-Z, 0-9, !@#%^&*_ + -join ((48..57) + (64..90) + 35 + (37..38) + 33 + 42 + 94 + 95 + (97..122) | Get-Random -Count 18 | ForEach-Object{[char] $_}) ) Write-Host -NoNewLine "Server password: """ @@ -1586,7 +1599,7 @@ function Invoke-RemoteDesktopServer { throw "Password complexity is too weak. Please choose a password following following rules:`r`n` * Minimum 12 Characters`r`n` - * One of following symbols: ""!@#$%^&*_""`r`n` + * One of following symbols: ""!@#%^&*_""`r`n` * At least of lower case character`r`n` * At least of upper case character`r`n" } @@ -1635,11 +1648,16 @@ function Invoke-RemoteDesktopServer # Remote Viewer will then need to establish a new session from scratch. $clientControl = $server.PullClient(10 * 1000); + # 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) diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 index a94d55a..b644c7f 100644 Binary files a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 and b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 differ diff --git a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 index 4ec5d9c..612a7d5 100644 --- a/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 +++ b/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1 @@ -50,10 +50,9 @@ -------------------------------------------------------------------------------#> Add-Type -Assembly System.Windows.Forms -Add-Type -MemberDefinition '[DllImport("gdi32.dll")] public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);' -Name GDI32 -Namespace W; -Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern int GetDC(IntPtr hWnd);[DllImport("User32.dll")] public static extern int ReleaseDC(IntPtr hwnd, int hdc);[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W; +Add-Type -MemberDefinition '[DllImport("User32.dll")] public static extern bool SetProcessDPIAware();' -Name User32 -Namespace W; -$global:PowerRemoteDesktopVersion = "1.0.beta.3" +$global:PowerRemoteDesktopVersion = "1.0.3.beta.4" function Write-Banner { @@ -82,24 +81,6 @@ function Write-Banner Write-Host "" } -function Get-ResolutionScaleFactor -{ - <# - .SYNOPSIS - Return current screen scale factor - #> - - $hdc = [W.User32]::GetDC(0) - try - { - return [W.GDI32]::GetDeviceCaps($hdc, 117) / [W.GDI32]::GetDeviceCaps($hdc, 10) - } - finally - { - [W.User32]::ReleaseDC(0, $hdc) | Out-Null - } -} - function Get-SHA512FromString { <# @@ -518,13 +499,13 @@ class ClientIO { $sessionInformation = $jsonObject | ConvertFrom-Json if ( - (-not ($sessionInformation.PSobject.Properties.name -match "MachineName")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "Username")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "WindowsVersion")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "SessionId")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "TransportMode")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "Version")) -or - (-not ($sessionInformation.PSobject.Properties.name -match "ScreenInformation")) + (-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 "TransportMode")) -or + (-not ($sessionInformation.PSobject.Properties.name -contains "Version")) -or + (-not ($sessionInformation.PSobject.Properties.name -contains "Screens")) ) { throw "Invalid session information data." @@ -538,6 +519,81 @@ class ClientIO { You cannot use two different version between Viewer and Server." } + # Check if remote server have multiple screens + $selectedScreen = $null + + if ($sessionInformation.Screens.Length -gt 1) + { + 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 + } + + $selectedScreen = $sessionInformation.Screens | Where-Object -FilterScript { $_.Id -eq $choice } + + if (-not $selectedScreen) + { + Write-Host "Invalid choice, please choose an existing screen index." -ForegroundColor Red + } + } + + if ($selectedScreen) + { + $this.Writer.WriteLine($selectedScreen.Name) + + break + } + } + } + else + { + $selectedScreen = $sessionInformation.Screens | Select-Object -First 1 + } + + if (-not $selectedScreen) + { + throw "No screen to capture." + } + + Write-Verbose "@SelectedScreen:" + Write-Verbose $selectedScreen + Write-Verbose "---" + + $sessionInformation | Add-Member -MemberType NoteProperty -Name "Screen" -Value $selectedScreen + return $sessionInformation } @@ -656,6 +712,11 @@ class ViewerSession $this.SessionInformation = $this.ClientDesktop.Hello() + if (-not $this.SessionInformation) + { + throw "Session cannot be null." + } + Write-Verbose "Open secondary tunnel for input control..." $this.ClientControl = [ClientIO]::new($this.ServerAddress, $this.ServerPort, $this.TLSv1_3) @@ -881,10 +942,7 @@ function Invoke-RemoteDesktopViewer Write-Banner - if (Get-ResolutionScaleFactor -ne 1) - { - [W.User32]::SetProcessDPIAware() - } + [W.User32]::SetProcessDPIAware() | Out-Null Write-Verbose "Server address: ""${ServerAddress}:${ServerPort}""" @@ -915,8 +973,8 @@ function Invoke-RemoteDesktopViewer $localScreenHeight -= $captionHeight $requireResize = ( - ($localScreenWidth -le $session.SessionInformation.ScreenInformation.Width) -or - ($localScreenHeight -le $session.SessionInformation.ScreenInformation.Height) + ($localScreenWidth -le $session.SessionInformation.Screen.Width) -or + ($localScreenHeight -le $session.SessionInformation.Screen.Height) ) $virtualDesktopWidth = 0 @@ -932,23 +990,23 @@ function Invoke-RemoteDesktopViewer { $virtualDesktopWidth = [math]::Round(($localScreenWidth * $resizeRatio) / 100) - $remoteResizedRatio = [math]::Round(($virtualDesktopWidth * 100) / $session.SessionInformation.ScreenInformation.Width) + $remoteResizedRatio = [math]::Round(($virtualDesktopWidth * 100) / $session.SessionInformation.Screen.Width) - $virtualDesktopHeight = [math]::Round(($session.SessionInformation.ScreenInformation.Height * $remoteResizedRatio) / 100) + $virtualDesktopHeight = [math]::Round(($session.SessionInformation.Screen.Height * $remoteResizedRatio) / 100) } else { $virtualDesktopHeight = [math]::Round(($localScreenHeight * $resizeRatio) / 100) - $remoteResizedRatio = [math]::Round(($virtualDesktopHeight * 100) / $session.SessionInformation.ScreenInformation.Height) + $remoteResizedRatio = [math]::Round(($virtualDesktopHeight * 100) / $session.SessionInformation.Screen.Height) - $virtualDesktopWidth = [math]::Round(($session.SessionInformation.ScreenInformation.Width * $remoteResizedRatio) / 100) + $virtualDesktopWidth = [math]::Round(($session.SessionInformation.Screen.Width * $remoteResizedRatio) / 100) } } else { - $virtualDesktopWidth = $session.SessionInformation.ScreenInformation.Width - $virtualDesktopHeight = $session.SessionInformation.ScreenInformation.Height + $virtualDesktopWidth = $session.SessionInformation.Screen.Width + $virtualDesktopHeight = $session.SessionInformation.Screen.Height } # Size Virtual Desktop Form Window @@ -1088,8 +1146,8 @@ function Invoke-RemoteDesktopViewer $Y = ($Y * 100) / $resizeRatio } - $X += $session.SessionInformation.ScreenInformation.X - $Y += $session.SessionInformation.ScreenInformation.Y + $X += $session.SessionInformation.Screen.X + $Y += $session.SessionInformation.Screen.Y $command = (New-MouseCommand -X $X -Y $Y -Button $Button -Type $Type) diff --git a/README.md b/README.md index c537072..5ec1f20 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ Supported options: * `TransportMode`: (Raw or Base64) Tell server how to send desktop image to remote viewer. Best method is Raw Bytes but I decided to keep the Base64 transport method as an alternative. * `TLSv1_3`: Define whether or not TLS v1.3 must be used for communication with Viewer. * `DisableVerbosity`: Disable verbosity (not recommended) +* `ImageQuality`: JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. If no certificate option is set, then a default X509 Certificate is generated and installed on local machine (Requires Administrative Privilege) @@ -220,7 +221,7 @@ Then pass the encoded string to parameter `EncodedCertificate`. ## Changelog -### 11 January 2021 (1.0.1 Beta 2) +### 11 January 2022 (1.0.1 Beta 2) * Desktop images are now transported in raw bytes instead of base64 string thus slightly improving performances. Base64 Transport Method is still available through an option but disabled by default. * Protocol has drastically changed. It is smoother to read and less prone to errors. @@ -230,10 +231,17 @@ Then pass the encoded string to parameter `EncodedCertificate`. * Possibility to disable verbose. * Server & Viewer version synchronization. Same version must be used between the two. -### 12 January 2021 (1.0.2 Beta 3) +### 12 January 2022 (1.0.2 Beta 3) * HDPI is completely supported. +### 12 January 2022 (1.0.3 Beta 4) + +* Possibility to change desktop image quality. +* Possibility to choose which screen to capture if multiple screens (Monitors) are present on remote machine. + +![Multi Screen Example](Assets/multi-screen.png) + ### List of ideas and TODO * 🟢 Do a deep investigation about SecureString and if it applies to current project (to protect password) @@ -246,7 +254,6 @@ Then pass the encoded string to parameter `EncodedCertificate`. * 🟠 Improve Virtual Keyboard. * 🟠 Server Concurrency. * 🟠 Listen for local/remote screen resolution update event. -* 🟠 Multiple Monitor Support. * 🔴 Motion Update for Desktop Streaming (Only send and update changing parts of desktop). 🟢 = Easy