├── Assets ├── PowerShell_Core_6.0_icon.png ├── demo.png ├── icon.png ├── iconv2-1.png ├── image31.png ├── multi-screen.png ├── prdp-banner.png └── server-fingerprint-validation.png ├── LICENSE ├── PowerRemoteDesktop_Server ├── PowerRemoteDesktop_Server.psd1 └── PowerRemoteDesktop_Server.psm1 ├── PowerRemoteDesktop_Viewer ├── PowerRemoteDesktop_Viewer.psd1 └── PowerRemoteDesktop_Viewer.psm1 ├── README.md ├── TestServer.ps1 └── TestViewer.ps1 /Assets/PowerShell_Core_6.0_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/PowerShell_Core_6.0_icon.png -------------------------------------------------------------------------------- /Assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/demo.png -------------------------------------------------------------------------------- /Assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/icon.png -------------------------------------------------------------------------------- /Assets/iconv2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/iconv2-1.png -------------------------------------------------------------------------------- /Assets/image31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/image31.png -------------------------------------------------------------------------------- /Assets/multi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/multi-screen.png -------------------------------------------------------------------------------- /Assets/prdp-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/prdp-banner.png -------------------------------------------------------------------------------- /Assets/server-fingerprint-validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/Assets/server-fingerprint-validation.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/PowerRemoteDesktop_Server/PowerRemoteDesktop_Server.psd1 -------------------------------------------------------------------------------- /PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhrozenIO/PowerRemoteDesktop/1e28a561489ae58e9bdb8b991e27b92336e218cc/PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psd1 -------------------------------------------------------------------------------- /PowerRemoteDesktop_Viewer/PowerRemoteDesktop_Viewer.psm1: -------------------------------------------------------------------------------- 1 | <#------------------------------------------------------------------------------- 2 | 3 | Power Remote Desktop 4 | 5 | In loving memory of my father. 6 | Thanks for all you've done. 7 | you will remain in my heart forever. 8 | 9 | .Developer 10 | Jean-Pierre LESUEUR (@DarkCoderSc) 11 | https://www.twitter.com/darkcodersc 12 | https://github.com/DarkCoderSc 13 | www.phrozen.io 14 | jplesueur@phrozen.io 15 | PHROZEN 16 | 17 | .License 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | .Disclaimer 23 | We are doing our best to prepare the content of this app. However, PHROZEN SASU and / or 24 | Jean-Pierre LESUEUR cannot warranty the expressions and suggestions of the contents, 25 | as well as its accuracy. In addition, to the extent permitted by the law, 26 | PHROZEN SASU and / or Jean-Pierre LESUEUR shall not be responsible for any losses 27 | and/or damages due to the usage of the information on our app. 28 | 29 | By using our app, you hereby consent to our disclaimer and agree to its terms. 30 | 31 | Any links contained in our app may lead to external sites are provided for 32 | convenience only. Any information or statements that appeared in these sites 33 | or app are not sponsored, endorsed, or otherwise approved by PHROZEN SASU and / or 34 | Jean-Pierre LESUEUR. For these external sites, PHROZEN SASU and / or Jean-Pierre LESUEUR 35 | cannot be held liable for the availability of, or the content located on or through it. 36 | Plus, any losses or damages occurred from using these contents or the internet 37 | generally. 38 | 39 | -------------------------------------------------------------------------------#> 40 | 41 | Add-Type -Assembly System.Windows.Forms 42 | 43 | Add-Type @" 44 | using System; 45 | using System.Runtime.InteropServices; 46 | 47 | public static class User32 48 | { 49 | [DllImport("User32.dll")] 50 | public static extern bool SetProcessDPIAware(); 51 | } 52 | "@ 53 | 54 | $global:PowerRemoteDesktopVersion = "4.0.0" 55 | 56 | $global:HostSyncHash = [HashTable]::Synchronized(@{ 57 | host = $host 58 | ClipboardText = (Get-Clipboard -Raw) 59 | }) 60 | 61 | $global:EphemeralTrustedServers = @() 62 | 63 | $global:LocalStoragePath = "HKCU:\SOFTWARE\PowerRemoteDesktop_Viewer" 64 | $global:LocalStoragePath_TrustedServers = -join($global:LocalStoragePath, "\TrustedServers") 65 | 66 | enum ClipboardMode { 67 | Disabled = 1 68 | Receive = 2 69 | Send = 3 70 | Both = 4 71 | } 72 | 73 | enum ProtocolCommand { 74 | Success = 1 75 | Fail = 2 76 | RequestSession = 3 77 | AttachToSession = 4 78 | BadRequest = 5 79 | ResourceFound = 6 80 | ResourceNotFound = 7 81 | LogonUIAccessDenied = 8 82 | LogonUIWrongSession = 9 83 | } 84 | 85 | enum WorkerKind { 86 | Desktop = 1 87 | Events = 2 88 | } 89 | 90 | enum BlockSize { 91 | Size32 = 32 92 | Size64 = 64 93 | Size96 = 96 94 | Size128 = 128 95 | Size256 = 256 96 | Size512 = 512 97 | } 98 | 99 | enum PacketSize { 100 | Size1024 = 1024 101 | Size2048 = 2048 102 | Size4096 = 4096 103 | Size8192 = 8192 104 | Size9216 = 9216 105 | Size12288 = 12288 106 | Size16384 = 16384 107 | } 108 | 109 | function Write-Banner 110 | { 111 | <# 112 | .SYNOPSIS 113 | Output cool information about current PowerShell module to terminal. 114 | #> 115 | 116 | Write-Host "" 117 | Write-Host "Power Remote Desktop - Version " -NoNewLine 118 | Write-Host $global:PowerRemoteDesktopVersion -ForegroundColor Cyan 119 | Write-Host "Jean-Pierre LESUEUR (" -NoNewLine 120 | Write-Host "@DarkCoderSc" -NoNewLine -ForegroundColor Green 121 | Write-Host ") " -NoNewLine 122 | Write-Host "#" -NoNewLine -ForegroundColor Blue 123 | Write-Host "#" -NoNewLine -ForegroundColor White 124 | Write-Host "#" -ForegroundColor Red 125 | Write-Host "https://" -NoNewLine -ForegroundColor Green 126 | Write-Host "www.github.com/darkcodersc" 127 | Write-Host "https://" -NoNewLine -ForegroundColor Green 128 | Write-Host "www.phrozen.io" 129 | Write-Host "" 130 | Write-Host "License: Apache License (Version 2.0, January 2004)" 131 | Write-Host "https://" -NoNewLine -ForegroundColor Green 132 | Write-Host "www.apache.org/licenses/" 133 | Write-Host "" 134 | } 135 | 136 | function Get-BooleanAnswer 137 | { 138 | <# 139 | .SYNOPSIS 140 | As user to make a boolean choice. Return True if Y and False if N. 141 | #> 142 | while ($true) 143 | { 144 | $choice = Read-Host "[Y] Yes [N] No (Default is ""N"")" 145 | if (-not $choice) 146 | { 147 | $choice = "N" 148 | } 149 | 150 | switch ($choice) 151 | { 152 | "Y" 153 | { 154 | return $true 155 | } 156 | 157 | "N" 158 | { 159 | return $false 160 | } 161 | 162 | default 163 | { 164 | Write-Host "Invalid Answer, available options are ""Y , N""" -ForegroundColor Red 165 | } 166 | } 167 | } 168 | } 169 | 170 | function New-RegistryStorage 171 | { 172 | <# 173 | .SYNOPSIS 174 | Create required registry keys for storing persistent data between viewer 175 | sessions. 176 | 177 | .DESCRIPTION 178 | Users doesn't share this storage. If you really wish to, replace HKCU by HKLM (Requires Admin Privilege) 179 | #> 180 | 181 | try 182 | { 183 | if (-not (Test-Path -Path $global:LocalStoragePath)) 184 | { 185 | Write-Verbose "Create local storage root at ""${global:LocalStoragePath}""..." 186 | 187 | New-Item -Path $global:LocalStoragePath 188 | } 189 | 190 | if (-not (Test-Path -Path $global:LocalStoragePath_TrustedServers)) 191 | { 192 | Write-Verbose "Create local storage child: ""${global:LocalStoragePath}""..." 193 | 194 | New-Item -Path $global:LocalStoragePath_TrustedServers 195 | } 196 | } 197 | catch 198 | { 199 | Write-Verbose "Could not write server fingerprint to local storage with error: ""$($_)""" 200 | } 201 | } 202 | 203 | function Write-ServerFingerprintToLocalStorage 204 | { 205 | <# 206 | .SYNOPSIS 207 | Write a trusted server certificate fingerprint to our local storage. 208 | 209 | .PARAMETER Fingerprint 210 | Type: String 211 | Default: None 212 | Description: Fingerprint to store in local storage. 213 | #> 214 | param ( 215 | [Parameter(Mandatory=$True)] 216 | [string] $Fingerprint 217 | ) 218 | 219 | New-RegistryStorage 220 | 221 | # Value is stored as a JSON Object to be easily upgraded and extended in future. 222 | $value = New-Object -TypeName PSCustomObject -Property @{ 223 | FirstSeen = (Get-Date).ToString() 224 | } 225 | 226 | New-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -PropertyType "String" -Value ($value | ConvertTo-Json -Compress) -ErrorAction Ignore 227 | } 228 | 229 | function Remove-TrustedServer 230 | { 231 | <# 232 | .SYNOPSIS 233 | Remove trusted server from local storage. 234 | 235 | .PARAMETER Fingerprint 236 | Type: String 237 | Default: None 238 | Description: Fingerprint to remove from local storage. 239 | #> 240 | param ( 241 | [Parameter(Mandatory=$True)] 242 | [string] $Fingerprint 243 | ) 244 | 245 | if (-not (Test-ServerFingerprintFromLocalStorage -Fingerprint $Fingerprint)) 246 | { 247 | throw "Could not find fingerprint on trusted server list." 248 | } 249 | 250 | Write-Host "You are about to permanently delete trusted server -> """ -NoNewline 251 | Write-Host $Fingerprint -NoNewLine -ForegroundColor Green 252 | Write-Host """" 253 | 254 | Write-Host "Are you sure ?" 255 | 256 | if (Get-BooleanAnswer) 257 | { 258 | Remove-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint 259 | 260 | Write-Host "Server successfully untrusted." 261 | } 262 | } 263 | 264 | function Get-TrustedServers 265 | { 266 | <# 267 | .SYNOPSIS 268 | Return a list of trusted servers fingerprints from local storage. 269 | #> 270 | 271 | $list = @() 272 | 273 | Get-Item -Path $global:LocalStoragePath_TrustedServers -ErrorAction Ignore | Select-Object -ExpandProperty Property | ForEach-Object { 274 | try 275 | { 276 | $list += New-Object -TypeName PSCustomObject -Property @{ 277 | Fingerprint = $_ 278 | Detail = (Get-ItemPropertyValue -Path $global:LocalStoragePath_TrustedServers -Name $_) | ConvertFrom-Json 279 | } 280 | } 281 | catch 282 | { } 283 | } 284 | 285 | return $list 286 | } 287 | 288 | function Clear-TrustedServers 289 | { 290 | <# 291 | .SYNOPSIS 292 | Remove all trusted servers from local storage. 293 | #> 294 | 295 | $trustedServers = Get-TrustedServers 296 | if (@($trustedServers).Length -eq 0) 297 | { 298 | throw "No trusted servers so far." 299 | } 300 | 301 | Write-Host "You are about to permanently delete $(@(trustedServers).Length) trusted servers." 302 | Write-Host "Are you sure ?" 303 | 304 | if (Get-BooleanAnswer) 305 | { 306 | Remove-Item -Path $global:LocalStoragePath_TrustedServers -Force -Verbose 307 | 308 | Write-Host "Servers successfully untrusted." 309 | } 310 | } 311 | 312 | function Test-ServerFingerprintFromLocalStorage 313 | { 314 | <# 315 | .SYNOPSIS 316 | Check if a server certificate fingerprint was saved to local storage. 317 | 318 | .PARAMETER Fingerprint 319 | Type: String 320 | Default: None 321 | Description: Fingerprint to check in local storage. 322 | #> 323 | param ( 324 | [Parameter(Mandatory=$True)] 325 | [string] $Fingerprint 326 | ) 327 | 328 | return (Get-ItemProperty -Path $global:LocalStoragePath_TrustedServers -Name $Fingerprint -ErrorAction Ignore) 329 | } 330 | 331 | function Get-SHA512FromString 332 | { 333 | <# 334 | .SYNOPSIS 335 | Return the SHA512 value from string. 336 | 337 | .PARAMETER String 338 | Type: String 339 | Default : None 340 | Description: A String to hash. 341 | 342 | .EXAMPLE 343 | Get-SHA512FromString -String "Hello, World" 344 | #> 345 | param ( 346 | [Parameter(Mandatory=$True)] 347 | [string] $String 348 | ) 349 | 350 | $buffer = [IO.MemoryStream]::new([byte[]][char[]]$String) 351 | 352 | return (Get-FileHash -InputStream $buffer -Algorithm SHA512).Hash 353 | } 354 | 355 | function Resolve-AuthenticationChallenge 356 | { 357 | <# 358 | .SYNOPSIS 359 | Algorithm to solve the server challenge during password authentication. 360 | 361 | .DESCRIPTION 362 | Server needs to resolve the challenge and keep the solution in memory before sending 363 | the candidate to remote peer. 364 | 365 | .PARAMETER Password 366 | Type: SecureString 367 | Default: None 368 | Description: Secure String object containing the password for resolving challenge. 369 | 370 | .PARAMETER Candidate 371 | Type: String 372 | Default: None 373 | Description: 374 | Random string used to solve the challenge. This string is public and is set across network by server. 375 | Each time a new connection is requested to server, a new candidate is generated. 376 | 377 | .EXAMPLE 378 | Resolve-AuthenticationChallenge -Password "s3cr3t!" -Candidate "rKcjdh154@]=Ldc" 379 | #> 380 | param ( 381 | [Parameter(Mandatory=$True)] 382 | [SecureString] $SecurePassword, 383 | 384 | [Parameter(Mandatory=$True)] 385 | [string] $Candidate 386 | ) 387 | 388 | $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword) 389 | try 390 | { 391 | $solution = -join($Candidate, ":", [Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)) 392 | 393 | for ([int] $i = 0; $i -le 1000; $i++) 394 | { 395 | $solution = Get-SHA512FromString -String $solution 396 | } 397 | 398 | return $solution 399 | } 400 | finally 401 | { 402 | [Runtime.InteropServices.Marshal]::FreeBSTR($BSTR) 403 | } 404 | } 405 | 406 | class ClientIO { 407 | [string] $RemoteAddress 408 | [int] $RemotePort 409 | [bool] $UseTLSv1_3 410 | 411 | [System.Net.Sockets.TcpClient] $Client = $null 412 | [System.Net.Security.SslStream] $SSLStream = $null 413 | [System.IO.StreamWriter] $Writer = $null 414 | [System.IO.StreamReader] $Reader = $null 415 | [System.IO.BinaryReader] $BinaryReader = $null 416 | 417 | ClientIO( 418 | [string] $RemoteAddress = "127.0.0.1", 419 | [int] $RemotePort = 2801, 420 | [bool] $UseTLSv1_3 = $false 421 | ) { 422 | $this.RemoteAddress = $RemoteAddress 423 | $this.RemotePort = $RemotePort 424 | $this.UseTLSv1_3 = $UseTLSv1_3 425 | } 426 | 427 | [void]Connect() { 428 | <# 429 | .SYNOPSIS 430 | Open a new connection to remote server. 431 | Create required streams and open a new secure connection with peer. 432 | #> 433 | Write-Verbose "Connect: ""$($this.RemoteAddress):$($this.RemotePort)...""" 434 | 435 | $this.Client = New-Object System.Net.Sockets.TcpClient($this.RemoteAddress, $this.RemotePort) 436 | 437 | Write-Verbose "Connected." 438 | 439 | if ($this.UseTLSv1_3) 440 | { 441 | $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS13 442 | } 443 | else { 444 | $TLSVersion = [System.Security.Authentication.SslProtocols]::TLS12 445 | } 446 | 447 | Write-Verbose "Establish an encrypted tunnel using: ${TLSVersion}..." 448 | 449 | $this.SSLStream = New-object System.Net.Security.SslStream( 450 | $this.Client.GetStream(), 451 | $false, 452 | { 453 | param( 454 | $Sendr, 455 | $Certificate, 456 | $Chain, 457 | $Policy 458 | ) 459 | 460 | if ( 461 | (Test-ServerFingerprintFromLocalStorage -Fingerprint $Certificate.Thumbprint) -or 462 | $global:EphemeralTrustedServers -contains $Certificate.Thumbprint 463 | ) 464 | { 465 | Write-Verbose "Fingerprint already known and trusted: ""$($Certificate.Thumbprint)""" 466 | 467 | return $true 468 | } 469 | else 470 | { 471 | Write-Verbose "@Remote Server Certificate:" 472 | Write-Verbose $Certificate 473 | Write-Verbose "---" 474 | 475 | Write-Host "Untrusted Server Certificate Fingerprint: """ -NoNewLine 476 | Write-Host $Certificate.Thumbprint -NoNewline -ForegroundColor Green 477 | Write-Host """" 478 | 479 | while ($true) 480 | { 481 | Write-Host "`r`nDo you want to trust current server ?" 482 | $choice = Read-Host "[A] Always [Y] Yes [N] No [?] Help (Default is ""N"")" 483 | if (-not $choice) 484 | { 485 | $choice = "N" 486 | } 487 | 488 | switch ($choice) 489 | { 490 | "?" 491 | { 492 | Write-Host "" 493 | 494 | Write-Host "[" -NoNewLine 495 | Write-Host "A" -NoNewLine -ForegroundColor Cyan 496 | Write-Host "] Always trust current server (Persistent between PowerShell Instances)" 497 | 498 | Write-Host "[" -NoNewLine 499 | Write-Host "Y" -NoNewLine -ForegroundColor Cyan 500 | Write-Host "] Trust current server during current PowerShell Instance lifetime (Temporary)." 501 | 502 | Write-Host "[" -NoNewLine 503 | Write-Host "N" -NoNewLine -ForegroundColor Cyan 504 | Write-Host "] Don't trust current server. Connection is aborted (Recommeneded if you don't recognize server fingerprint)." 505 | 506 | Write-Host "[" -NoNewLine 507 | Write-Host "?" -NoNewLine -ForegroundColor Cyan 508 | Write-Host "] Current help output." 509 | 510 | Write-Host "" 511 | } 512 | 513 | "A" 514 | { 515 | Write-ServerFingerprintToLocalStorage -Fingerprint $Certificate.Thumbprint 516 | 517 | return $true 518 | } 519 | 520 | "Y" 521 | { 522 | $global:EphemeralTrustedServers += $Certificate.Thumbprint 523 | 524 | return $true 525 | } 526 | 527 | "N" 528 | { 529 | return $false 530 | } 531 | 532 | default 533 | { 534 | Write-Host "Invalid Answer, available options are ""A , Y , N , H""" -ForegroundColor Red 535 | } 536 | } 537 | } 538 | } 539 | } 540 | ) 541 | 542 | $this.SSLStream.AuthenticateAsClient( 543 | "PowerRemoteDesktop", 544 | $null, 545 | $TLSVersion, 546 | $null 547 | ) 548 | 549 | if (-not $this.SSLStream.IsEncrypted) 550 | { 551 | throw "Could not establish a secure communication channel with remote server." 552 | } 553 | 554 | $this.SSLStream.WriteTimeout = 5000 555 | 556 | $this.Writer = New-Object System.IO.StreamWriter($this.SSLStream) 557 | $this.Writer.AutoFlush = $true 558 | 559 | $this.Reader = New-Object System.IO.StreamReader($this.SSLStream) 560 | 561 | $this.BinaryReader = New-Object System.IO.BinaryReader($this.SSLStream) 562 | 563 | Write-Verbose "Encrypted tunnel opened and ready for use." 564 | } 565 | 566 | [void]Authentify([SecureString] $SecurePassword) { 567 | <# 568 | .SYNOPSIS 569 | Handle authentication process with remote peer. 570 | 571 | .PARAMETER Password 572 | Type: SecureString 573 | Default: None 574 | Description: Secure String object containing the password. 575 | 576 | .EXAMPLE 577 | .Authentify((ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force)) 578 | #> 579 | 580 | Write-Verbose "Authentify with remote server (Challenged-Based Authentication)..." 581 | 582 | $candidate = $this.Reader.ReadLine() 583 | 584 | $challengeSolution = Resolve-AuthenticationChallenge -Candidate $candidate -SecurePassword $SecurePassword 585 | 586 | Write-Verbose "@Challenge:" 587 | Write-Verbose "Candidate: ""${candidate}""" 588 | Write-Verbose "Solution: ""${challengeSolution}""" 589 | Write-Verbose "---" 590 | 591 | $this.Writer.WriteLine($challengeSolution) 592 | 593 | $result = $this.Reader.ReadLine() 594 | if ($result -eq [ProtocolCommand]::Success) 595 | { 596 | Write-Verbose "Solution accepted. Authentication success." 597 | } 598 | else 599 | { 600 | throw "Solution declined. Authentication failed." 601 | } 602 | 603 | } 604 | 605 | [string] RemoteAddress() { 606 | return $this.Client.Client.RemoteEndPoint.Address 607 | } 608 | 609 | [int] RemotePort() { 610 | return $this.Client.Client.RemoteEndPoint.Port 611 | } 612 | 613 | [string] LocalAddress() { 614 | return $this.Client.Client.LocalEndPoint.Address 615 | } 616 | 617 | [int] LocalPort() { 618 | return $this.Client.Client.LocalEndPoint.Port 619 | } 620 | 621 | [string] ReadLine([int] $Timeout) 622 | { 623 | <# 624 | .SYNOPSIS 625 | Read string message from remote peer with timeout support. 626 | 627 | .PARAMETER Timeout 628 | Type: Integer 629 | Description: Maximum period of time to wait for incomming data. 630 | #> 631 | $defautTimeout = $this.SSLStream.ReadTimeout 632 | try 633 | { 634 | $this.SSLStream.ReadTimeout = $Timeout 635 | 636 | return $this.Reader.ReadLine() 637 | } 638 | finally 639 | { 640 | $this.SSLStream.ReadTimeout = $defautTimeout 641 | } 642 | } 643 | 644 | [string] ReadLine() 645 | { 646 | <# 647 | .SYNOPSIS 648 | Shortcut to Reader ReadLine method. No timeout support. 649 | #> 650 | return $this.Reader.ReadLine() 651 | } 652 | 653 | [void] WriteJson([PSCustomObject] $Object) 654 | { 655 | <# 656 | .SYNOPSIS 657 | Transform a PowerShell Object as a JSON Representation then send to remote 658 | peer. 659 | 660 | .PARAMETER Object 661 | Type: PSCustomObject 662 | Description: Object to be serialized in JSON. 663 | #> 664 | 665 | $this.Writer.WriteLine(($Object | ConvertTo-Json -Compress)) 666 | } 667 | 668 | [void] WriteLine([string] $Value) 669 | { 670 | $this.Writer.WriteLine($Value) 671 | } 672 | 673 | [PSCustomObject] ReadJson([int] $Timeout) 674 | { 675 | <# 676 | .SYNOPSIS 677 | Read json string from remote peer and attempt to deserialize as a PowerShell Object. 678 | 679 | .PARAMETER Timeout 680 | Type: Integer 681 | Description: Maximum period of time to wait for incomming data. 682 | #> 683 | return ($this.ReadLine($Timeout) | ConvertFrom-Json) 684 | } 685 | 686 | [PSCustomObject] ReadJson() 687 | { 688 | <# 689 | .SYNOPSIS 690 | Alternative to ReadJson without timeout support. 691 | #> 692 | return ($this.ReadLine() | ConvertFrom-Json) 693 | } 694 | 695 | [void]Close() { 696 | <# 697 | .SYNOPSIS 698 | Release Streams and Connections. 699 | #> 700 | if ($this.Writer) 701 | { 702 | $this.Writer.Close() 703 | } 704 | 705 | if ($this.Reader) 706 | { 707 | $this.Reader.Close() 708 | } 709 | 710 | if ($this.BinaryReader) 711 | { 712 | $this.BinaryReader.Close() 713 | } 714 | 715 | if ($this.SSLStream) 716 | { 717 | $this.SSLStream.Close() 718 | } 719 | 720 | if ($this.Client) 721 | { 722 | $this.Client.Close() 723 | } 724 | } 725 | } 726 | 727 | class ViewerConfiguration 728 | { 729 | [bool] $RequireResize = $false 730 | [int] $RemoteDesktopWidth = 0 731 | [int] $RemoteDesktopHeight = 0 732 | [int] $VirtualDesktopWidth = 0 733 | [int] $VirtualDesktopHeight = 0 734 | [int] $ScreenX_Delta = 0 735 | [int] $ScreenY_Delta = 0 736 | [float] $ScreenX_Ratio = 1 737 | [float] $ScreenY_Ratio = 1 738 | } 739 | 740 | class ViewerSession 741 | { 742 | [PSCustomObject] $ServerInformation = $null 743 | [ViewerConfiguration] $ViewerConfiguration = $null 744 | 745 | [string] $ServerAddress = "127.0.0.1" 746 | [string] $ServerPort = 2801 747 | [SecureString] $SecurePassword = $null 748 | [bool] $UseTLSv1_3 = $false 749 | [int] $ImageCompressionQuality = 100 750 | [int] $ResizeRatio = 0 751 | [PacketSize] $PacketSize = [PacketSize]::Size9216 752 | [BlockSize] $BlockSize = [BlockSize]::Size64 753 | [bool] $LogonUI = $false 754 | 755 | [ClientIO] $ClientDesktop = $null 756 | [ClientIO] $ClientEvents = $null 757 | 758 | ViewerSession( 759 | [string] $ServerAddress, 760 | [int] $ServerPort, 761 | [SecureString] $SecurePassword 762 | ) 763 | { 764 | # Or: System.Management.Automation.Runspaces.MaxPort (High(Word)) 765 | if ($ServerPort -lt 0 -and $ServerPort -gt 65535) 766 | { 767 | throw "Invalid TCP Port (0-65535)" 768 | } 769 | 770 | $this.ServerAddress = $ServerAddress 771 | $this.ServerPort = $ServerPort 772 | $this.SecurePassword = $SecurePassword 773 | } 774 | 775 | [void] OpenSession() { 776 | <# 777 | .SYNOPSIS 778 | Request a new session with remote server. 779 | #> 780 | 781 | Write-Verbose "Request new session with remote server: ""$($this.ServerAddress):$($this.ServerPort)""..." 782 | 783 | if ($this.ServerInformation) 784 | { 785 | throw "A session already exists." 786 | } 787 | 788 | Write-Verbose "Establish first contact with remote server..." 789 | 790 | $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) 791 | try 792 | { 793 | $client.Connect() 794 | 795 | $client.Authentify($this.SecurePassword) 796 | 797 | Write-Verbose "Request session..." 798 | 799 | $client.WriteLine(([ProtocolCommand]::RequestSession)) 800 | 801 | $this.ServerInformation = $client.ReadJson() 802 | 803 | Write-Verbose "@ServerInformation:" 804 | Write-Verbose $this.ServerInformation 805 | Write-Verbose "---" 806 | 807 | if ( 808 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "SessionId")) -or 809 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "Version")) -or 810 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "ViewOnly")) -or 811 | 812 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "MachineName")) -or 813 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "Username")) -or 814 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "WindowsVersion")) -or 815 | (-not ($this.ServerInformation.PSobject.Properties.name -contains "Screens")) 816 | ) 817 | { 818 | throw "Invalid server information object." 819 | } 820 | 821 | Write-Verbose "Server informations acknowledged, prepare and send our expectation..." 822 | 823 | if ($this.ServerInformation.Version -ne $global:PowerRemoteDesktopVersion) 824 | { 825 | throw "Server and Viewer version mismatch.`r`n` 826 | Local: ""${global:PowerRemoteDesktopVersion}""`r`n` 827 | Remote: ""$($this.ServerInformation.Version)""`r`n` 828 | You cannot use two different version between Viewer and Server." 829 | } 830 | 831 | if ($this.ServerInformation.ViewOnly) 832 | { 833 | Write-Host "You are accessing a read-only desktop." -ForegroundColor Cyan 834 | } 835 | 836 | # Define which screen we want to capture 837 | $selectedScreen = $null 838 | 839 | if ($this.ServerInformation.Screens.Length -gt 1) 840 | { 841 | Write-Verbose "Remote server have $($this.ServerInformation.Screens.Length) screens." 842 | 843 | Write-Host "Remote server have " -NoNewLine 844 | Write-Host $($this.ServerInformation.Screens.Length) -NoNewLine -ForegroundColor Green 845 | Write-Host " different screens:`r`n" 846 | 847 | foreach ($screen in $this.ServerInformation.Screens) 848 | { 849 | Write-Host $screen.Id -NoNewLine -ForegroundColor Cyan 850 | Write-Host " - $($screen.Name)" -NoNewLine 851 | 852 | if ($screen.Primary) 853 | { 854 | Write-Host " (" -NoNewLine 855 | Write-Host "Primary" -NoNewLine -ForegroundColor Cyan 856 | Write-Host ")" -NoNewLine 857 | } 858 | 859 | Write-Host "" 860 | } 861 | 862 | while ($true) 863 | { 864 | $choice = Read-Host "`r`nPlease choose which screen index to capture (Default: Primary)" 865 | 866 | if (-not $choice) 867 | { 868 | # Select-Object -First 1 should also grab the Primary Screen (Since it is ordered). 869 | $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Primary -eq $true } 870 | } 871 | else 872 | { 873 | if (-not $choice -is [int]) { 874 | Write-Host "You must enter a valid index (integer), starting at 1." 875 | 876 | continue 877 | } 878 | 879 | $selectedScreen = $this.ServerInformation.Screens | Where-Object -FilterScript { $_.Id -eq $choice } 880 | 881 | if (-not $selectedScreen) 882 | { 883 | Write-Host "Invalid choice, please choose an existing screen index." -ForegroundColor Red 884 | } 885 | } 886 | 887 | if ($selectedScreen) 888 | { 889 | break 890 | } 891 | } 892 | } 893 | else 894 | { 895 | $selectedScreen = $this.ServerInformation.Screens | Select-Object -First 1 896 | } 897 | 898 | # Define our Virtual Desktop Form constraints 899 | $localScreenWidth = Get-LocalScreenWidth 900 | $localScreenHeight = (Get-LocalScreenHeight) - (Get-WindowCaptionHeight) 901 | 902 | $this.ViewerConfiguration = [ViewerConfiguration]::New() 903 | 904 | $this.ViewerConfiguration.RemoteDesktopWidth = $selectedScreen.Width 905 | $this.ViewerConfiguration.RemoteDesktopHeight = $selectedScreen.Height 906 | 907 | # If remote screen is bigger than local screen, we will resize remote screen to fit 90% of local screen. 908 | # Supports screen orientation (Horizontal / Vertical) 909 | if ($localScreenWidth -le $selectedScreen.Width -or $localScreenHeight -le $selectedScreen.Height) 910 | { 911 | $adjustRatio = 90 912 | 913 | $adjustVertically = $localScreenWidth -gt $localScreenHeight 914 | 915 | if ($adjustVertically) 916 | { 917 | $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($localScreenWidth * $adjustRatio) / 100) 918 | 919 | $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopWidth * 100) / $selectedScreen.Width) 920 | 921 | $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($selectedScreen.Height * $remoteResizedRatio) / 100) 922 | } 923 | else 924 | { 925 | $this.ViewerConfiguration.VirtualDesktopHeight = [math]::Round(($localScreenHeight * $adjustRatio) / 100) 926 | 927 | $remoteResizedRatio = [math]::Round(($this.ViewerConfiguration.VirtualDesktopHeight * 100) / $selectedScreen.Height) 928 | 929 | $this.ViewerConfiguration.VirtualDesktopWidth = [math]::Round(($selectedScreen.Width * $remoteResizedRatio) / 100) 930 | } 931 | } 932 | else 933 | { 934 | $this.ViewerConfiguration.VirtualDesktopWidth = $selectedScreen.Width 935 | $this.ViewerConfiguration.VirtualDesktopHeight = $selectedScreen.Height 936 | } 937 | 938 | # If remote desktop resize is forced, we apply defined ratio to current configuration 939 | if ($this.ResizeRatio -ge 30 -and $this.ResizeRatio -le 99) 940 | { 941 | $this.ViewerConfiguration.VirtualDesktopWidth = ($selectedScreen.Width * $this.ResizeRatio) / 100 942 | $this.ViewerConfiguration.VirtualDesktopHeight = ($selectedScreen.Height * $this.ResizeRatio) / 100 943 | } 944 | 945 | $this.ViewerConfiguration.RequireResize = $this.ViewerConfiguration.VirtualDesktopWidth -ne $selectedScreen.Width -or 946 | $this.ViewerConfiguration.VirtualDesktopHeight -ne $selectedScreen.Height 947 | 948 | $this.ViewerConfiguration.ScreenX_Delta = $selectedScreen.X 949 | $this.ViewerConfiguration.ScreenY_Delta = $selectedScreen.Y 950 | 951 | if ($this.ViewerConfiguration.RequireResize) 952 | { 953 | $this.ViewerConfiguration.ScreenX_Ratio = $selectedScreen.Width / $this.ViewerConfiguration.VirtualDesktopWidth 954 | $this.ViewerConfiguration.ScreenY_Ratio = $selectedScreen.Height / $this.ViewerConfiguration.VirtualDesktopHeight 955 | } 956 | 957 | $viewerExpectation = New-Object PSCustomObject -Property @{ 958 | ScreenName = $selectedScreen.Name 959 | ImageCompressionQuality = $this.ImageCompressionQuality 960 | PacketSize = $this.PacketSize 961 | BlockSize = $this.BlockSize 962 | LogonUI = $this.LogonUI 963 | } 964 | 965 | Write-Verbose "@ViewerExpectation:" 966 | Write-Verbose $viewerExpectation 967 | Write-Verbose "---" 968 | 969 | $client.WriteJson($viewerExpectation) 970 | 971 | switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) 972 | { 973 | ([ProtocolCommand]::Success) 974 | { 975 | break 976 | } 977 | 978 | ([ProtocolCommand]::LogonUIAccessDenied) 979 | { 980 | throw "Could not access LogonUI / Winlogon desktop.`r`n" + 981 | "To access LogonUI desktop, you must have ""NT AUTHORITY/System"" privilege in current active session." 982 | 983 | break 984 | } 985 | 986 | ([ProtocolCommand]::LogonUIWrongSession) 987 | { 988 | throw "Could not access LogonUI / Winlogon desktop.`r`n" 989 | "To access LogonUI desktop, server process must be running under active Windows Session." 990 | 991 | break 992 | } 993 | 994 | default 995 | { 996 | throw "Remote server did not acknoledged our expectation in time." 997 | } 998 | } 999 | } 1000 | catch 1001 | { 1002 | $this.CloseSession() 1003 | 1004 | throw "Could not open a new session with error: ""$($_)""" 1005 | } 1006 | finally 1007 | { 1008 | if ($client) 1009 | { 1010 | $client.Close() 1011 | } 1012 | } 1013 | } 1014 | 1015 | [ClientIO] ConnectWorker([WorkerKind] $WorkerKind) 1016 | { 1017 | Write-Verbose "Connect new worker: ""$WorkerKind""..." 1018 | 1019 | $this.CheckSession() 1020 | 1021 | $client = [ClientIO]::New($this.ServerAddress, $this.ServerPort, $this.UseTLSv1_3) 1022 | try 1023 | { 1024 | $client.Connect() 1025 | 1026 | $client.Authentify($this.SecurePassword) 1027 | 1028 | $client.WriteLine(([ProtocolCommand]::AttachToSession)) 1029 | 1030 | Write-Verbose "Attach worker to remote session ""$($this.ServerInformation.SessionId)""" 1031 | 1032 | $client.WriteLine($this.ServerInformation.SessionId) 1033 | 1034 | switch ([ProtocolCommand] $client.ReadLine(5 * 1000)) 1035 | { 1036 | ([ProtocolCommand]::ResourceFound) 1037 | { 1038 | Write-Verbose "Worker successfully attached to session, define which kind of worker we are..." 1039 | 1040 | $client.WriteLine($WorkerKind) 1041 | 1042 | Write-Verbose "Worker ready." 1043 | 1044 | break 1045 | } 1046 | 1047 | ([ProtocolCommand]::ResourceNotFound) 1048 | { 1049 | throw "Server could not locate session." 1050 | } 1051 | 1052 | default 1053 | { 1054 | throw "Unexpected answer from remote server for session attach." 1055 | } 1056 | } 1057 | 1058 | return $client 1059 | } 1060 | catch 1061 | { 1062 | if ($client) 1063 | { 1064 | $client.Close() 1065 | } 1066 | 1067 | throw "Could not connect worker with error: $($_)" 1068 | } 1069 | } 1070 | 1071 | [void] ConnectDesktopWorker() 1072 | { 1073 | Write-Verbose "Create new desktop streaming worker..." 1074 | 1075 | $this.ClientDesktop = $this.ConnectWorker([WorkerKind]::Desktop) 1076 | } 1077 | 1078 | [void] ConnectEventsWorker() 1079 | { 1080 | Write-Verbose "Create new event event (in/out) worker..." 1081 | 1082 | $this.ClientEvents = $this.ConnectWorker([WorkerKind]::Events) 1083 | } 1084 | 1085 | [bool] HasSession() 1086 | { 1087 | return $this.ServerInformation -and $this.ViewerConfiguration 1088 | } 1089 | 1090 | [void] CheckSession() 1091 | { 1092 | if (-not $this.HasSession) 1093 | { 1094 | throw "Session is missing." 1095 | } 1096 | } 1097 | 1098 | [void] CloseSession() { 1099 | <# 1100 | .SYNOPSIS 1101 | Close an existing session with remote server. 1102 | Terminate active connections and reset session informations. 1103 | #> 1104 | 1105 | Write-Verbose "Close existing session..." 1106 | 1107 | if ($this.ClientDesktop) 1108 | { 1109 | $this.ClientDesktop.Close() 1110 | } 1111 | 1112 | if ($this.ClientEvents) 1113 | { 1114 | $this.ClientEvents.Close() 1115 | } 1116 | 1117 | $this.ClientDesktop = $null 1118 | $this.ClientEvents = $null 1119 | 1120 | $this.ServerInformation = $null 1121 | $this.ViewerConfiguration = $null 1122 | 1123 | Write-Verbose "Session closed." 1124 | } 1125 | 1126 | } 1127 | 1128 | $global:VirtualDesktopUpdaterScriptBlock = { 1129 | try 1130 | { 1131 | $packetSize = [int]$Param.packetSize 1132 | 1133 | # SizeOf(DWORD) * 3 (SizeOf(Desktop) + SizeOf(Left) + SizeOf(Top)) 1134 | $struct = New-Object -TypeName byte[] -ArgumentList (([Runtime.InteropServices.Marshal]::SizeOf([System.Type][UInt32])) * 3) 1135 | 1136 | $stream = New-Object System.IO.MemoryStream 1137 | 1138 | $scene = $null 1139 | $sceneGraphics = $null 1140 | 1141 | $destPoint = [System.Drawing.Point]::New(0, 0) 1142 | 1143 | $scene = [System.Drawing.Bitmap]::New( 1144 | $Param.ViewerConfiguration.RemoteDesktopWidth, 1145 | $Param.ViewerConfiguration.RemoteDesktopHeight 1146 | ) 1147 | 1148 | $sceneGraphics = [System.Drawing.Graphics]::FromImage($scene) 1149 | $sceneGraphics.CompositingMode = [System.Drawing.Drawing2D.CompositingMode]::SourceCopy 1150 | 1151 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Image = $scene # Assign our scene 1152 | 1153 | # Wait until the virtual desktop form is shown to user desktop. 1154 | while (-not $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.Visible) 1155 | { 1156 | Start-Sleep -Milliseconds 100 1157 | } 1158 | 1159 | # Tiny hack to correctly bring to front window, this is the most effective technique so far. 1160 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $true 1161 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.TopMost = $false 1162 | 1163 | while ($true) 1164 | { 1165 | try 1166 | { 1167 | $null = $Param.Client.SSLStream.Read($struct, 0, $struct.Length) 1168 | 1169 | $totalBufferSize = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x0) 1170 | $destPoint.X = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x4) 1171 | $destPoint.Y = [System.Runtime.InteropServices.Marshal]::ReadInt32($struct, 0x8) 1172 | 1173 | $stream.SetLength($totalBufferSize) 1174 | 1175 | $stream.Position = 0 1176 | do 1177 | { 1178 | $bufferSize = $stream.Length - $stream.Position 1179 | if ($bufferSize -gt $packetSize) 1180 | { 1181 | $bufferSize = $packetSize 1182 | } 1183 | 1184 | $null = $stream.Write($Param.Client.BinaryReader.ReadBytes($bufferSize), 0, $bufferSize) 1185 | } until ($stream.Position -eq $stream.Length) 1186 | 1187 | if ($stream.Length -eq 0) 1188 | { 1189 | continue 1190 | } 1191 | 1192 | # Next Iterations 1193 | $sceneGraphics.DrawImage( 1194 | [System.Drawing.Image]::FromStream($stream), 1195 | $destPoint 1196 | ) 1197 | 1198 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Invalidate() 1199 | } 1200 | catch 1201 | { 1202 | break 1203 | } 1204 | } 1205 | } 1206 | finally 1207 | { 1208 | if ($scene) 1209 | { 1210 | $scene.Dispose() 1211 | } 1212 | 1213 | if ($sceneGraphics) 1214 | { 1215 | $sceneGraphics.Dispose() 1216 | } 1217 | 1218 | if ($stream) 1219 | { 1220 | $stream.Close() 1221 | } 1222 | 1223 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Form.Close() 1224 | } 1225 | } 1226 | 1227 | $global:IngressEventScriptBlock = { 1228 | 1229 | enum CursorType { 1230 | IDC_APPSTARTING 1231 | IDC_ARROW 1232 | IDC_CROSS 1233 | IDC_HAND 1234 | IDC_HELP 1235 | IDC_IBEAM 1236 | IDC_ICON 1237 | IDC_NO 1238 | IDC_SIZE 1239 | IDC_SIZEALL 1240 | IDC_SIZENESW 1241 | IDC_SIZENS 1242 | IDC_SIZENWSE 1243 | IDC_SIZEWE 1244 | IDC_UPARROW 1245 | IDC_WAIT 1246 | } 1247 | 1248 | enum InputEvent { 1249 | KeepAlive = 0x1 1250 | MouseCursorUpdated = 0x2 1251 | ClipboardUpdated = 0x3 1252 | DesktopActive = 0x4 1253 | DesktopInactive = 0x5 1254 | } 1255 | 1256 | enum ClipboardMode { 1257 | Disabled = 1 1258 | Receive = 2 1259 | Send = 3 1260 | Both = 4 1261 | } 1262 | 1263 | while ($true) 1264 | { 1265 | try 1266 | { 1267 | $jsonEvent = $Param.Client.Reader.ReadLine() 1268 | } 1269 | catch 1270 | { break } 1271 | 1272 | try 1273 | { 1274 | $aEvent = $jsonEvent | ConvertFrom-Json 1275 | } 1276 | catch 1277 | { continue } 1278 | 1279 | if (-not ($aEvent.PSobject.Properties.name -match "Id")) 1280 | { continue } 1281 | 1282 | switch ([InputEvent] $aEvent.Id) 1283 | { 1284 | # Remote Global Mouse Cursor State Changed (Icon) 1285 | ([InputEvent]::MouseCursorUpdated) 1286 | { 1287 | if (-not ($aEvent.PSobject.Properties.name -match "Cursor")) 1288 | { continue } 1289 | 1290 | $cursor = [System.Windows.Forms.Cursors]::Arrow 1291 | 1292 | switch ([CursorType] $aEvent.Cursor) 1293 | { 1294 | ([CursorType]::IDC_APPSTARTING) { $cursor = [System.Windows.Forms.Cursors]::AppStarting } 1295 | ([CursorType]::IDC_CROSS) { $cursor = [System.Windows.Forms.Cursors]::Cross } 1296 | ([CursorType]::IDC_HAND) { $cursor = [System.Windows.Forms.Cursors]::Hand } 1297 | ([CursorType]::IDC_HELP) { $cursor = [System.Windows.Forms.Cursors]::Help } 1298 | ([CursorType]::IDC_IBEAM) { $cursor = [System.Windows.Forms.Cursors]::IBeam } 1299 | ([CursorType]::IDC_NO) { $cursor = [System.Windows.Forms.Cursors]::No } 1300 | ([CursorType]::IDC_SIZENESW) { $cursor = [System.Windows.Forms.Cursors]::SizeNESW } 1301 | ([CursorType]::IDC_SIZENS) { $cursor = [System.Windows.Forms.Cursors]::SizeNS } 1302 | ([CursorType]::IDC_SIZENWSE) { $cursor = [System.Windows.Forms.Cursors]::SizeNWSE } 1303 | ([CursorType]::IDC_SIZEWE) { $cursor = [System.Windows.Forms.Cursors]::SizeWE } 1304 | ([CursorType]::IDC_UPARROW) { $cursor = [System.Windows.Forms.Cursors]::UpArrow } 1305 | ([CursorType]::IDC_WAIT) { $cursor = [System.Windows.Forms.Cursors]::WaitCursor } 1306 | 1307 | {( $_ -eq ([CursorType]::IDC_SIZE) -or $_ -eq ([CursorType]::IDC_SIZEALL) )} 1308 | { 1309 | $cursor = [System.Windows.Forms.Cursors]::SizeAll 1310 | } 1311 | } 1312 | 1313 | try 1314 | { 1315 | $Param.VirtualDesktopSyncHash.VirtualDesktop.Picture.Cursor = $cursor 1316 | } 1317 | catch 1318 | {} 1319 | 1320 | break 1321 | } 1322 | 1323 | ([InputEvent]::ClipboardUpdated) 1324 | { 1325 | if ($Param.Clipboard -eq ([ClipboardMode]::Disabled) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) 1326 | { continue } 1327 | 1328 | if (-not ($aEvent.PSobject.Properties.name -match "Text")) 1329 | { continue } 1330 | 1331 | $HostSyncHash.ClipboardText = $aEvent.Text 1332 | 1333 | Set-Clipboard -Value $aEvent.Text 1334 | 1335 | break 1336 | } 1337 | 1338 | ([InputEvent]::DesktopActive) 1339 | { 1340 | break 1341 | } 1342 | 1343 | ([InputEvent]::DesktopInactive) 1344 | { 1345 | break 1346 | } 1347 | } 1348 | } 1349 | } 1350 | 1351 | $global:EgressEventScriptBlock = { 1352 | 1353 | enum OutputEvent { 1354 | # 0x1 0x2 0x3 are at another place (GUI Thread) 1355 | KeepAlive = 0x4 1356 | ClipboardUpdated = 0x5 1357 | } 1358 | 1359 | enum ClipboardMode { 1360 | Disabled = 1 1361 | Receive = 2 1362 | Send = 3 1363 | Both = 4 1364 | } 1365 | 1366 | function Send-Event 1367 | { 1368 | <# 1369 | .SYNOPSIS 1370 | Send an event to remote peer. 1371 | 1372 | .PARAMETER AEvent 1373 | Define what kind of event to send. 1374 | 1375 | .PARAMETER Data 1376 | An optional object containing additional information about the event. 1377 | #> 1378 | param ( 1379 | [Parameter(Mandatory=$True)] 1380 | [OutputEvent] $AEvent, 1381 | 1382 | [PSCustomObject] $Data = $null 1383 | ) 1384 | 1385 | try 1386 | { 1387 | if (-not $Data) 1388 | { 1389 | $Data = New-Object -TypeName PSCustomObject -Property @{ 1390 | Id = $AEvent 1391 | } 1392 | } 1393 | else 1394 | { 1395 | $Data | Add-Member -MemberType NoteProperty -Name "Id" -Value $AEvent 1396 | } 1397 | 1398 | $Param.OutputEventSyncHash.Writer.WriteLine(($Data | ConvertTo-Json -Compress)) 1399 | 1400 | return $true 1401 | } 1402 | catch 1403 | { 1404 | return $false 1405 | } 1406 | } 1407 | 1408 | $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() 1409 | 1410 | while ($true) 1411 | { 1412 | # Events that occurs every seconds needs to be placed bellow. 1413 | # If no event has occured during this second we send a Keep-Alive signal to 1414 | # remote peer and detect a potential socket disconnection. 1415 | if ($stopWatch.ElapsedMilliseconds -ge 1000) 1416 | { 1417 | try 1418 | { 1419 | $eventTriggered = $false 1420 | 1421 | if ($Param.Clipboard -eq ([ClipboardMode]::Both) -or $Param.Clipboard -eq ([ClipboardMode]::Send)) 1422 | { 1423 | # IDEA: Check for existing clipboard change event or implement a custom clipboard 1424 | # change detector using "WM_CLIPBOARDUPDATE" for example (WITHOUT INLINE C#) 1425 | # It is not very important but it would avoid calling "Get-Clipboard" every seconds. 1426 | $currentClipboard = (Get-Clipboard -Raw) 1427 | 1428 | if ($currentClipboard -and $currentClipboard -cne $HostSyncHash.ClipboardText) 1429 | { 1430 | $data = New-Object -TypeName PSCustomObject -Property @{ 1431 | Text = $currentClipboard 1432 | } 1433 | 1434 | if (-not (Send-Event -AEvent ([OutputEvent]::ClipboardUpdated) -Data $data)) 1435 | { break } 1436 | 1437 | $HostSyncHash.ClipboardText = $currentClipboard 1438 | 1439 | $eventTriggered = $true 1440 | } 1441 | } 1442 | 1443 | # Send a Keep-Alive if during this second iteration nothing happened. 1444 | if (-not $eventTriggered) 1445 | { 1446 | if (-not (Send-Event -AEvent ([OutputEvent]::KeepAlive))) 1447 | { break } 1448 | } 1449 | } 1450 | finally 1451 | { 1452 | $stopWatch.Restart() 1453 | } 1454 | } 1455 | } 1456 | } 1457 | 1458 | function Get-WindowCaptionHeight 1459 | { 1460 | $form = New-Object System.Windows.Forms.Form 1461 | try { 1462 | $screenRect = $form.RectangleToScreen($form.ClientRectangle) 1463 | 1464 | return $screenRect.Top - $virtualDesktopSyncHash.VirtualDesktop.Form.Top 1465 | } 1466 | finally 1467 | { 1468 | if ($form) 1469 | { 1470 | $form.Dispose() 1471 | } 1472 | } 1473 | } 1474 | 1475 | function Get-LocalScreenWidth 1476 | { 1477 | return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Width 1478 | } 1479 | 1480 | function Get-LocalScreenHeight 1481 | { 1482 | return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea.Height 1483 | } 1484 | 1485 | function New-VirtualDesktopForm 1486 | { 1487 | <# 1488 | .SYNOPSIS 1489 | Create new WinForms Components to handle Virtual Desktop. 1490 | 1491 | .DESCRIPTION 1492 | This function first create a new Windows Form then create a new child component (PaintBox) 1493 | to display remote desktop frames. 1494 | 1495 | It returns a PowerShell object containing both Form and PaintBox. 1496 | 1497 | .PARAMETER Width 1498 | Type: Integer 1499 | Default: 1200 1500 | Description: The pre-defined width of new form 1501 | 1502 | .PARAMETER Height 1503 | Type: Integer 1504 | Default: 800 1505 | Description: The pre-defined height of new form 1506 | 1507 | .PARAMETER Caption 1508 | Type: String 1509 | Default: PowerRemoteDesktop Viewer 1510 | Description: The pre-defined caption of new form. 1511 | 1512 | .EXAMPLE 1513 | New-VirtualDesktopForm -Caption "New Desktop Form" -Width 1200 -Height 800 1514 | #> 1515 | param ( 1516 | [int] $Width = 1200, 1517 | [int] $Height = 800, 1518 | [string] $Caption = "PowerRemoteDesktop Viewer" 1519 | ) 1520 | 1521 | $form = New-Object System.Windows.Forms.Form 1522 | 1523 | $form.Width = $Width 1524 | $form.Height = $Height 1525 | $form.BackColor = [System.Drawing.Color]::Black 1526 | $form.Text = $Caption 1527 | $form.KeyPreview = $true # Necessary to capture keystrokes. 1528 | $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedSingle 1529 | $form.MaximizeBox = $false 1530 | 1531 | $pictureBox = New-Object System.Windows.Forms.PictureBox 1532 | $pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill 1533 | $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::StretchImage 1534 | 1535 | $form.Controls.Add($pictureBox) 1536 | 1537 | return New-Object PSCustomObject -Property @{ 1538 | Form = $form 1539 | Picture = $pictureBox 1540 | } 1541 | } 1542 | 1543 | function New-RunSpace 1544 | { 1545 | <# 1546 | .SYNOPSIS 1547 | Create a new PowerShell Runspace. 1548 | 1549 | .DESCRIPTION 1550 | Notice: the $host variable is used for debugging purpose to write on caller PowerShell 1551 | Terminal. 1552 | 1553 | .PARAMETER ScriptBlock 1554 | Type: ScriptBlock 1555 | Default: None 1556 | Description: Instructions to execute in new runspace. 1557 | 1558 | .PARAMETER Param 1559 | Type: PSCustomObject 1560 | Default: None 1561 | Description: Object to attach in runspace context. 1562 | 1563 | .EXAMPLE 1564 | New-RunSpace -Client $newClient -ScriptBlock { Start-Sleep -Seconds 10 } 1565 | #> 1566 | 1567 | param( 1568 | [Parameter(Mandatory=$True)] 1569 | [ScriptBlock] $ScriptBlock, 1570 | 1571 | [PSCustomObject] $Param = $null 1572 | ) 1573 | 1574 | $runspace = [RunspaceFactory]::CreateRunspace() 1575 | $runspace.ThreadOptions = "ReuseThread" 1576 | $runspace.ApartmentState = "STA" 1577 | $runspace.Open() 1578 | 1579 | if ($Param) 1580 | { 1581 | $runspace.SessionStateProxy.SetVariable("Param", $Param) 1582 | } 1583 | 1584 | $runspace.SessionStateProxy.SetVariable("HostSyncHash", $global:HostSyncHash) 1585 | 1586 | $powershell = [PowerShell]::Create().AddScript($ScriptBlock) 1587 | 1588 | $powershell.Runspace = $runspace 1589 | 1590 | $asyncResult = $powershell.BeginInvoke() 1591 | 1592 | return New-Object PSCustomObject -Property @{ 1593 | Runspace = $runspace 1594 | PowerShell = $powershell 1595 | AsyncResult = $asyncResult 1596 | } 1597 | } 1598 | 1599 | function Invoke-RemoteDesktopViewer 1600 | { 1601 | <# 1602 | .SYNOPSIS 1603 | Open a new remote desktop session with a remote server. 1604 | 1605 | .DESCRIPTION 1606 | Notice: Prefer using SecurePassword over plain-text password even if a plain-text password is getting converted to SecureString anyway. 1607 | 1608 | .PARAMETER ServerAddress 1609 | Type: String 1610 | Default: 127.0.0.1 1611 | Description: Remote server host/address. 1612 | 1613 | .PARAMETER ServerPort 1614 | Type: Integer 1615 | Default: 2801 (0 - 65535) 1616 | Description: Remote server port. 1617 | 1618 | .PARAMETER SecurePassword 1619 | Type: SecureString 1620 | Default: None 1621 | Description: SecureString object containing password used to authenticate with remote server (Recommended) 1622 | 1623 | .PARAMETER Password 1624 | Type: String 1625 | Default: None 1626 | Description: Plain-Text Password used to authenticate with remote server (Not recommended, use SecurePassword instead) 1627 | 1628 | .PARAMETER UseTLSv1_3 1629 | Type: Switch 1630 | Default: False 1631 | Description: If present, TLS v1.3 will be used instead of TLS v1.2 (Recommended if applicable to both systems) 1632 | 1633 | .PARAMETER DisableVerbosity 1634 | Type: Boolean 1635 | Default: False 1636 | Description: If present, program wont show verbosity messages. 1637 | 1638 | .PARAMETER Clipboard 1639 | Type: Enum 1640 | Default: Both 1641 | Description: 1642 | Define clipboard synchronization mode (Both, Disabled, Send, Receive) see bellow for more detail. 1643 | 1644 | * Disabled -> Clipboard synchronization is disabled in both side 1645 | * Receive -> Only incomming clipboard is allowed 1646 | * Send -> Only outgoing clipboard is allowed 1647 | * Both -> Clipboard synchronization is allowed on both side 1648 | 1649 | .PARAMETER ImageCompressionQuality 1650 | Type: Integer (0 - 100) 1651 | Default: 75 1652 | Description: JPEG Compression level from 0 to 100. 0 = Lowest quality, 100 = Highest quality. 1653 | 1654 | .PARAMETER Resize 1655 | Type: Switch 1656 | Default: None 1657 | Description: If present, remote desktop will get resized accordingly with ResizeRatio option. 1658 | 1659 | .PARAMETER ResizeRatio 1660 | Type: Integer (30 - 99) 1661 | Default: None 1662 | Description: Used with Resize option, define the resize ratio in percentage. 1663 | 1664 | .PARAMETER AlwaysOnTop 1665 | Type: Switch 1666 | Default: False 1667 | Description: If present, virtual desktop form will be above all other window's. 1668 | 1669 | .PARAMETER BlockSize 1670 | Type: Enum 1671 | Values: Size32, Size64, Size96, Size128, Size256, Size512 1672 | Default: Size64 1673 | Description: 1674 | (Advanced) Define the screen grid block size. 1675 | Choose the block size accordingly to remote screen size / computer constrainsts (CPU / Network) 1676 | 1677 | Size1024 -> 1024 Bytes (1KiB) 1678 | Size2048 -> 2048 Bytes (2KiB) 1679 | Size4096 -> 4096 Bytes (4KiB) 1680 | Size8192 -> 8192 Bytes (8KiB) 1681 | Size9216 -> 9216 Bytes (9KiB) 1682 | Size12288 -> 12288 Bytes (12KiB) 1683 | Size16384 -> 16384 Bytes (16KiB) 1684 | 1685 | .PARAMETER PacketSize 1686 | Type: Enum 1687 | Values: Size1024, Size2048, Size4096, Size8192, Size9216, Size12288, Size16384 1688 | Default: Size9216 1689 | Description: 1690 | (Advanced) Define the network packet size for streams. 1691 | Choose the packet size accordingly to your network constrainsts. 1692 | 1693 | Size32 -> 32x32 1694 | Size64 -> 64x64 1695 | Size96 -> 96x96 1696 | Size128 -> 128x128 1697 | Size256 -> 256x256 1698 | Size512 -> 512x512 1699 | 1700 | .PARAMETER LogonUI 1701 | Type: Switch 1702 | Default: None 1703 | Description: Request server to open LogonUI / Winlogon desktop insead of default user desktop (Requires SYSTEM privilege in active session). 1704 | 1705 | .EXAMPLE 1706 | Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -SecurePassword (ConvertTo-SecureString -String "s3cr3t!" -AsPlainText -Force) 1707 | Invoke-RemoteDesktopViewer -ServerAddress "192.168.0.10" -ServerPort "2801" -Password "s3cr3t!" 1708 | Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort "2801" -Password "Just4TestingLocally!" 1709 | 1710 | #> 1711 | param ( 1712 | [string] $ServerAddress = "127.0.0.1", 1713 | 1714 | [ValidateRange(0, 65535)] 1715 | [int] $ServerPort = 2801, 1716 | 1717 | [switch] $UseTLSv1_3, 1718 | [SecureString] $SecurePassword, 1719 | [String] $Password, 1720 | [switch] $DisableVerbosity, 1721 | [ClipboardMode] $Clipboard = [ClipboardMode]::Both, 1722 | 1723 | [ValidateRange(0, 100)] 1724 | [int] $ImageCompressionQuality = 75, 1725 | 1726 | [switch] $Resize, 1727 | 1728 | [ValidateRange(30, 99)] 1729 | [int] $ResizeRatio = 90, 1730 | 1731 | [switch] $AlwaysOnTop, 1732 | [PacketSize] $PacketSize = [PacketSize]::Size9216, 1733 | [BlockSize] $BlockSize = [BlockSize]::Size64, 1734 | [switch] $LogonUI 1735 | ) 1736 | 1737 | [System.Collections.Generic.List[PSCustomObject]]$runspaces = @() 1738 | 1739 | $oldErrorActionPreference = $ErrorActionPreference 1740 | $oldVerbosePreference = $VerbosePreference 1741 | try 1742 | { 1743 | $ErrorActionPreference = "stop" 1744 | 1745 | if (-not $DisableVerbosity) 1746 | { 1747 | $VerbosePreference = "continue" 1748 | } 1749 | else 1750 | { 1751 | $VerbosePreference = "SilentlyContinue" 1752 | } 1753 | 1754 | Write-Banner 1755 | 1756 | $null = [User32]::SetProcessDPIAware() 1757 | 1758 | Write-Verbose "Server address: ""${ServerAddress}:${ServerPort}""" 1759 | 1760 | if (-not $SecurePassword -and -not $Password) 1761 | { 1762 | throw "You must specify either a SecurePassword or Password parameter used during server authentication." 1763 | } 1764 | 1765 | if ($Password -and -not $SecurePassword) 1766 | { 1767 | $SecurePassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) 1768 | 1769 | Remove-Variable -Name "Password" -ErrorAction SilentlyContinue 1770 | } 1771 | 1772 | $session = [ViewerSession]::New( 1773 | $ServerAddress, 1774 | $ServerPort, 1775 | $SecurePassword 1776 | ) 1777 | try 1778 | { 1779 | $session.UseTLSv1_3 = $UseTLSv1_3 1780 | $session.ImageCompressionQuality = $ImageCompressionQuality 1781 | $session.PacketSize = $PacketSize 1782 | $session.BlockSize = $BlockSize 1783 | $session.LogonUI = $LogonUI 1784 | 1785 | if ($Resize) 1786 | { 1787 | $session.ResizeRatio = $ResizeRatio 1788 | } 1789 | 1790 | Write-Host "Start new remote desktop session..." 1791 | 1792 | $session.OpenSession() 1793 | 1794 | $session.ConnectDesktopWorker() 1795 | 1796 | $session.ConnectEventsWorker() 1797 | 1798 | Write-Host "Session successfully established, start streaming..." 1799 | 1800 | Write-Verbose "Create WinForms Environment..." 1801 | 1802 | $virtualDesktop = New-VirtualDesktopForm 1803 | $virtualDesktopSyncHash = [HashTable]::Synchronized(@{ 1804 | VirtualDesktop = $virtualDesktop 1805 | }) 1806 | 1807 | $virtualDesktop.Form.Text = [string]::Format( 1808 | "Power Remote Desktop v{0}: {1}/{2} - {3}", 1809 | $global:PowerRemoteDesktopVersion, 1810 | $session.ServerInformation.Username, 1811 | $session.ServerInformation.MachineName, 1812 | $session.ServerInformation.WindowsVersion 1813 | ) 1814 | 1815 | # Size Virtual Desktop Form Window 1816 | $virtualDesktop.Form.ClientSize = [System.Drawing.Size]::New( 1817 | $session.ViewerConfiguration.VirtualDesktopWidth, 1818 | $session.ViewerConfiguration.VirtualDesktopHeight 1819 | ) 1820 | 1821 | # Create a thread-safe hashtable to send events to remote server. 1822 | $outputEventSyncHash = [HashTable]::Synchronized(@{ 1823 | Writer = $session.ClientEvents.Writer 1824 | }) 1825 | 1826 | # WinForms Events (If enabled, I recommend to disable control when testing on local machine to avoid funny things) 1827 | if (-not $session.ServerInformation.ViewOnly) 1828 | { 1829 | enum OutputEvent { 1830 | Keyboard = 0x1 1831 | MouseClickMove = 0x2 1832 | MouseWheel = 0x3 1833 | } 1834 | 1835 | enum MouseState { 1836 | Up = 0x1 1837 | Down = 0x2 1838 | Move = 0x3 1839 | } 1840 | 1841 | function New-MouseEvent 1842 | { 1843 | <# 1844 | .SYNOPSIS 1845 | Generate a new mouse event object to be sent to server. 1846 | This event is used to simulate mouse move and clicks. 1847 | 1848 | .PARAMETER X 1849 | Type: Integer 1850 | Default: None 1851 | Description: The position of mouse in horizontal axis. 1852 | 1853 | .PARAMETER Y 1854 | Type: Integer 1855 | Default: None 1856 | Description: The position of mouse in vertical axis. 1857 | 1858 | .PARAMETER Type 1859 | Type: Enum 1860 | Default: None 1861 | Description: The type of mouse event (Example: Move, Click) 1862 | 1863 | .PARAMETER Button 1864 | Type: String 1865 | Default: None 1866 | Description: The pressed button on mouse (Example: Left, Right, Middle) 1867 | 1868 | .EXAMPLE 1869 | New-MouseEvent -X 10 -Y 35 -Type "Up" -Button "Left" 1870 | New-MouseEvent -X 10 -Y 35 -Type "Down" -Button "Left" 1871 | New-MouseEvent -X 100 -Y 325 -Type "Move" 1872 | #> 1873 | param ( 1874 | [Parameter(Mandatory=$true)] 1875 | [int] $X, 1876 | [Parameter(Mandatory=$true)] 1877 | [int] $Y, 1878 | [Parameter(Mandatory=$true)] 1879 | [MouseState] $Type, 1880 | 1881 | [string] $Button = "None" 1882 | ) 1883 | 1884 | return New-Object PSCustomObject -Property @{ 1885 | Id = [OutputEvent]::MouseClickMove 1886 | X = $X 1887 | Y = $Y 1888 | Button = $Button 1889 | Type = $Type 1890 | } 1891 | } 1892 | 1893 | function New-KeyboardEvent 1894 | { 1895 | <# 1896 | .SYNOPSIS 1897 | Generate a new keyboard event object to be sent to server. 1898 | This event is used to simulate keyboard strokes. 1899 | 1900 | .PARAMETER Keys 1901 | Type: String 1902 | Default: None 1903 | Description: Plain text keys to be simulated on remote computer. 1904 | 1905 | .EXAMPLE 1906 | New-KeyboardEvent -Keys "Hello, World" 1907 | New-KeyboardEvent -Keys "t" 1908 | #> 1909 | param ( 1910 | [Parameter(Mandatory=$true)] 1911 | [string] $Keys 1912 | ) 1913 | 1914 | return New-Object PSCustomObject -Property @{ 1915 | Id = [OutputEvent]::Keyboard 1916 | Keys = $Keys 1917 | } 1918 | } 1919 | 1920 | function Send-VirtualMouse 1921 | { 1922 | <# 1923 | .SYNOPSIS 1924 | Transform the virtual mouse (the one in Virtual Desktop Form) coordinates to real remote desktop 1925 | screen coordinates (especially when incomming desktop frames are resized) 1926 | 1927 | When event is generated, it is immediately sent to remote server. 1928 | 1929 | .PARAMETER X 1930 | Type: Integer 1931 | Default: None 1932 | Description: The position of virtual mouse in horizontal axis. 1933 | 1934 | .PARAMETER Y 1935 | Type: Integer 1936 | Default: None 1937 | Description: The position of virtual mouse in vertical axis. 1938 | 1939 | .PARAMETER Type 1940 | Type: Integer 1941 | Default: None 1942 | Description: The type of mouse event (Example: Move, Click) 1943 | 1944 | .PARAMETER Button 1945 | Type: String 1946 | Default: None 1947 | Description: The pressed button on mouse (Example: Left, Right, Middle) 1948 | 1949 | .EXAMPLE 1950 | Send-VirtualMouse -X 10 -Y 20 -Type "Move" 1951 | #> 1952 | param ( 1953 | [Parameter(Mandatory=$True)] 1954 | [int] $X, 1955 | [Parameter(Mandatory=$True)] 1956 | [int] $Y, 1957 | [Parameter(Mandatory=$True)] 1958 | [MouseState] $Type, 1959 | 1960 | [string] $Button = "" 1961 | ) 1962 | 1963 | if ($session.ViewerConfiguration.RequireResize) 1964 | { 1965 | $X *= $session.ViewerConfiguration.ScreenX_Ratio 1966 | $Y *= $session.ViewerConfiguration.ScreenY_Ratio 1967 | } 1968 | 1969 | $X += $session.ViewerConfiguration.ScreenX_Delta 1970 | $Y += $session.ViewerConfiguration.ScreenY_Delta 1971 | 1972 | $aEvent = (New-MouseEvent -X $X -Y $Y -Button $Button -Type $Type) 1973 | 1974 | try 1975 | { 1976 | $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) 1977 | } 1978 | catch 1979 | {} 1980 | } 1981 | 1982 | function Send-VirtualKeyboard 1983 | { 1984 | <# 1985 | .SYNOPSIS 1986 | Send to remote server key strokes to simulate. 1987 | 1988 | .PARAMETER KeyChain 1989 | Type: String 1990 | Default: None 1991 | Description: A string representing character(s) to simulate remotely. 1992 | 1993 | .EXAMPLE 1994 | Send-VirtualKeyboard -KeyChain "Hello, World" 1995 | Send-VirtualKeyboard -KeyChain "{LEFT}" 1996 | #> 1997 | param ( 1998 | [Parameter(Mandatory=$True)] 1999 | [string] $KeyChain 2000 | ) 2001 | 2002 | $aEvent = (New-KeyboardEvent -Keys $KeyChain) 2003 | 2004 | try 2005 | { 2006 | $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) 2007 | } 2008 | catch 2009 | {} 2010 | } 2011 | 2012 | $virtualDesktop.Form.Add_KeyPress( 2013 | { 2014 | if ($_.KeyChar) 2015 | { 2016 | switch -CaseSensitive ([string]$_.KeyChar) 2017 | { 2018 | "{" { $result = "{{}" } 2019 | "}" { $result = "{}}" } 2020 | "+" { $result = "{+}" } 2021 | "^" { $result = "{^}" } 2022 | "%" { $result = "{%}" } 2023 | "~" { $result = "{~}" } 2024 | "(" { $result = "{(}" } 2025 | ")" { $result = "{)}" } 2026 | "[" { $result = "{[}" } 2027 | "]" { $result = "{]}" } 2028 | 2029 | default { $result = $_ } 2030 | } 2031 | 2032 | Send-VirtualKeyboard -KeyChain $result 2033 | } 2034 | } 2035 | ) 2036 | 2037 | $virtualDesktop.Form.Add_Shown( 2038 | { 2039 | # Center Virtual Desktop Form 2040 | $virtualDesktop.Form.Location = [System.Drawing.Point]::New( 2041 | ((Get-LocalScreenWidth) - $virtualDesktop.Form.Width) / 2, 2042 | ((Get-LocalScreenHeight) - $virtualDesktop.Form.Height) / 2 2043 | ) 2044 | 2045 | $virtualDesktop.Form.TopMost = $AlwaysOnTop 2046 | } 2047 | ) 2048 | 2049 | $virtualDesktop.Form.Add_KeyDown( 2050 | { 2051 | $result = "" 2052 | 2053 | switch ($_.KeyValue) 2054 | { 2055 | # WIN Key 2056 | 91 { $result = "^{ESC}" } 2057 | 2058 | # F Keys 2059 | 112 { $result = "{F1}" } 2060 | 113 { $result = "{F2}" } 2061 | 114 { $result = "{F3}" } 2062 | 115 { $result = "{F4}" } 2063 | 116 { $result = "{F5}" } 2064 | 117 { $result = "{F6}" } 2065 | 118 { $result = "{F7}" } 2066 | 119 { $result = "{F8}" } 2067 | 120 { $result = "{F9}" } 2068 | 121 { $result = "{F10}" } 2069 | 122 { $result = "{F11}" } 2070 | 123 { $result = "{F12}" } 2071 | 124 { $result = "{F13}" } 2072 | 125 { $result = "{F14}" } 2073 | 126 { $result = "{F15}" } 2074 | 127 { $result = "{F16}" } 2075 | 2076 | # Arrows 2077 | 37 { $result = "{LEFT}" } 2078 | 38 { $result = "{UP}" } 2079 | 39 { $result = "{RIGHT}" } 2080 | 40 { $result = "{DOWN}" } 2081 | 2082 | # Misc 2083 | 92 { $result = "{WIN}" } 2084 | 27 { $result = "{ESC}"} 2085 | 33 { $result = "{PGUP}" } 2086 | 34 { $result = "{PGDW}" } 2087 | 36 { $result = "{HOME}" } 2088 | 46 { $result = "{DELETE}" } 2089 | 35 { $result = "{END}" } 2090 | 2091 | # Add other keys bellow 2092 | } 2093 | 2094 | if ($result) 2095 | { 2096 | Send-VirtualKeyboard -KeyChain $result 2097 | } 2098 | } 2099 | ) 2100 | 2101 | $virtualDesktop.Picture.Add_MouseDown( 2102 | { 2103 | Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Down) 2104 | } 2105 | ) 2106 | 2107 | $virtualDesktop.Picture.Add_MouseUp( 2108 | { 2109 | Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Up) 2110 | } 2111 | ) 2112 | 2113 | $virtualDesktop.Picture.Add_MouseMove( 2114 | { 2115 | Send-VirtualMouse -X $_.X -Y $_.Y -Button $_.Button -Type ([MouseState]::Move) 2116 | } 2117 | ) 2118 | 2119 | $virtualDesktop.Picture.Add_MouseWheel( 2120 | { 2121 | $aEvent = New-Object PSCustomObject -Property @{ 2122 | Id = [OutputEvent]::MouseWheel 2123 | Delta = $_.Delta 2124 | } 2125 | 2126 | try 2127 | { 2128 | $outputEventSyncHash.Writer.WriteLine(($aEvent | ConvertTo-Json -Compress)) 2129 | } 2130 | catch {} 2131 | } 2132 | ) 2133 | } 2134 | 2135 | Write-Verbose "Create runspace for desktop streaming..." 2136 | 2137 | $param = New-Object -TypeName PSCustomObject -Property @{ 2138 | Client = $session.ClientDesktop 2139 | VirtualDesktopSyncHash = $virtualDesktopSyncHash 2140 | ViewerConfiguration = $session.ViewerConfiguration 2141 | PacketSize = $session.PacketSize 2142 | } 2143 | 2144 | $newRunspace = (New-RunSpace -ScriptBlock $global:VirtualDesktopUpdaterScriptBlock -Param $param) 2145 | $runspaces.Add($newRunspace) 2146 | 2147 | Write-Verbose "Create runspace for incoming events..." 2148 | 2149 | $param = New-Object -TypeName PSCustomObject -Property @{ 2150 | Client = $session.ClientEvents 2151 | VirtualDesktopSyncHash = $virtualDesktopSyncHash 2152 | Clipboard = $Clipboard 2153 | } 2154 | 2155 | $newRunspace = (New-RunSpace -ScriptBlock $global:IngressEventScriptBlock -Param $param) 2156 | $runspaces.Add($newRunspace) 2157 | 2158 | Write-Verbose "Create runspace for outgoing events..." 2159 | 2160 | $param = New-Object -TypeName PSCustomObject -Property @{ 2161 | OutputEventSyncHash = $outputEventSyncHash 2162 | Clipboard = $Clipboard 2163 | } 2164 | 2165 | $newRunspace = (New-RunSpace -ScriptBlock $global:EgressEventScriptBlock -Param $param) 2166 | $runspaces.Add($newRunspace) 2167 | 2168 | Write-Verbose "Done. Showing Virtual Desktop Form." 2169 | 2170 | $null = $virtualDesktop.Form.ShowDialog() 2171 | } 2172 | finally 2173 | { 2174 | Write-Verbose "Free environement." 2175 | 2176 | if ($session) 2177 | { 2178 | $session.CloseSession() 2179 | 2180 | $session = $null 2181 | } 2182 | 2183 | Write-Verbose "Free runspaces..." 2184 | 2185 | foreach ($runspace in $runspaces) 2186 | { 2187 | $null = $runspace.PowerShell.EndInvoke($runspace.AsyncResult) 2188 | $runspace.PowerShell.Runspace.Dispose() 2189 | $runspace.PowerShell.Dispose() 2190 | } 2191 | $runspaces.Clear() 2192 | 2193 | if ($virtualDesktop) 2194 | { 2195 | $virtualDesktop.Form.Dispose() 2196 | } 2197 | 2198 | Write-Host "Remote desktop session has ended." 2199 | } 2200 | } 2201 | finally 2202 | { 2203 | $ErrorActionPreference = $oldErrorActionPreference 2204 | $VerbosePreference = $oldVerbosePreference 2205 | } 2206 | } 2207 | 2208 | try { 2209 | Export-ModuleMember -Function Remove-TrustedServer 2210 | Export-ModuleMember -Function Clear-TrustedServers 2211 | Export-ModuleMember -Function Get-TrustedServers 2212 | Export-ModuleMember -Function Invoke-RemoteDesktopViewer 2213 | } catch {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Power Remote Desktop 6 | 7 | 8 | 9 | ⚠️ As of August 2024, PowerRemoteDesktop has been rebranded as Arcane and is now available through [this official repository](https://github.com/PhrozenIO/Arcane). The current repository will no longer be maintained. Please be sure to bookmark the new repository and leave a star if you find it useful! 10 | 11 | Welcome to **Power Remote Desktop** for remote desktop access in pure PowerShell! This module offers a unique solution for remotely controlling one or multiple screens using only PowerShell. Unlike other remote desktop tools that rely on external protocols and software, our module utilizes its own remote desktop protocol. 12 | 13 | The module consists of both a client and a server component, both of which are written entirely in PowerShell. Our protocol provides secure, encrypted communication using TLS and offers both challenge-based password authentication and certificate-based authentication. 14 | 15 | In addition to providing full mouse and keyboard control over the remote desktop, our module also replicates the mouse cursor icon for the viewer, synchronizes the clipboard between the local and remote systems, and more. Despite the limitations of PowerShell, we have implemented techniques to optimize network traffic and improve the streaming experience, resulting in a smooth and efficient remote desktop experience. 16 | 17 | At the time of writing, this is the only known entirely PowerShell-based remote desktop application. We hope you find it useful and we welcome any feedback or suggestions you may have. 18 | 19 | Tested on: 20 | 21 | * **Windows 10** 22 | * **Windows 11** 23 | 24 | Current version: **4.0.0 Stable** 25 | 26 | ## Performance 27 | 28 | For a better streaming performance and overall experience, we recommend using PowerShell 7 instead of PowerShell 5. 29 | 30 | You can install PowerShell 7 for Windows [here](https://docs.microsoft.com/fr-fr/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.2) 31 | 32 | --- 33 | 34 | ## Highlighted Features 35 | 36 | 37 | 38 | * Remote Desktop Streaming: This feature allows you to stream the desktop of the remote computer to your own device. The streaming supports HDPI and scaling, providing a high-quality display on various screens and resolutions. 39 | * Remote Control: With this feature, you can control the mouse (including moves, clicks, and wheel) and keyboard of the remote computer as if you were sitting in front of it. 40 | * Secure: To protect the privacy and security of your remote desktop sessions, the module uses TLSv1.2 or 1.3 to encrypt the network traffic. Access to the server is granted through a challenge-based authentication mechanism that requires a user-defined complex password. 41 | * Network Traffic Encryption: The module supports encrypting the network traffic using either a default X509 certificate (which requires administrator privileges) or your own custom X509 certificate. 42 | * Server Certificate Fingerprint Validation: To ensure the authenticity of the server, the module allows you to validate the fingerprint of the server certificate and optionally persist this validation between sessions. 43 | * Clipboard Synchronization: This feature allows you to synchronize the clipboard text between the viewer (your device) and the server (the remote computer). You can easily copy and paste text between the two systems. 44 | * Mouse Cursor Icon Synchronization: The module also synchronizes the state of the mouse cursor icon between the viewer (virtual desktop) and the server, providing a more seamless and intuitive remote desktop experience. 45 | * Multi-Screen Support: If the remote computer has more than one desktop screen, you can choose which screen to capture and stream to your device. 46 | * View Only Mode: This feature allows you to disable remote control abilities and simply view the screen of the remote computer. It can be useful for demonstrations or presentations. 47 | * Session Concurrency: Multiple viewers can connect to a single server at the same time, allowing multiple users to collaborate on the same remote desktop. 48 | * Sleep Mode Prevention: To ensure that the remote desktop remains active and responsive, the module prevents the remote computer from entering sleep mode while it is waiting for viewers to connect. 49 | * Streaming Optimization: To improve the streaming speed, the module only sends updated pieces of the desktop to the viewer, reducing the amount of data transmitted over the network. 50 | 51 | --- 52 | 53 | ## Setup everything in less than a minute (Fast Setup) 54 | 55 | ````powershell 56 | Install-Module -Name PowerRemoteDesktop_Server 57 | 58 | Invoke-RemoteDesktopServer -CertificateFile "" 59 | ```` 60 | 61 | If you want to avoid using your own certificate and prefer not to go through the process of creating one, you can remove the 'CertificateFile' option and run PowerShell as an administrator instead. 62 | 63 | ````powershell 64 | Install-Module -Name PowerRemoteDesktop_Viewer 65 | 66 | Invoke-RemoteDesktopViewer -ServerAddress "" -Password "" 67 | ```` 68 | 69 | Thats it 😉 70 | 71 | --- 72 | 73 | ## Detailed Installation and Instructions 74 | 75 | There are several ways to use this PowerShell application. The recommended method is to install both the server and viewer components using the PowerShell Gallery. Alternatively, you can install them as modules or import them as scripts manually. Choose the method that best fits your needs and preferences. 76 | 77 | ### Install as a PowerShell Module from PowerShell Gallery (**Recommended**) 78 | 79 | You can install Power Remote Desktop from the PowerShell Gallery, which is similar to Aptitude for Debian or Brew for MacOS. To do so, run the following commands: 80 | 81 | ```powershell 82 | Install-Module -Name PowerRemoteDesktop_Server 83 | 84 | Install-Module -Name PowerRemoteDesktop_Viewer 85 | ``` 86 | 87 | `AllowPrerelease` is mandatory when current version is marked as a *Prerelease* 88 | 89 | When you run the command, you may see the following warning in your command prompt: 90 | 91 | ``` 92 | Untrusted repository 93 | You are installing the modules from an untrusted repository. If you trust this repository, change its 94 | InstallationPolicy value by running the Set-PSRepository cmdlet. Are you sure you want to install the modules from 95 | 'PSGallery'? 96 | ``` 97 | 98 | Type 'Y' to confirm and proceed with the installation. When the installation is complete, both modules should be available. You can verify this by running the following command: 99 | 100 | ```powershell 101 | Get-Module -ListAvailable 102 | ``` 103 | 104 | Example Output: 105 | 106 | ``` 107 | PS C:\Users\Phrozen\Desktop> Get-Module -ListAvailable 108 | 109 | 110 | Directory: C:\Users\Phrozen\Documents\WindowsPowerShell\Modules 111 | 112 | 113 | ModuleType Version Name ExportedCommands 114 | ---------- ------- ---- ---------------- 115 | Manifest 1.0.0 PowerRemoteDesktop_Server Invoke-RemoteDesktopServer 116 | Manifest 1.0.0 PowerRemoteDesktop_Viewer Invoke-RemoteDesktopViewer 117 | 118 | <..snip..> 119 | ``` 120 | 121 | If the modules are not showing up, try running the following commands and then check again: 122 | 123 | ```powershell 124 | Import-Module PowerRemoteDesktop_Server 125 | 126 | Import-Module PowerRemoteDesktop_Viewer 127 | ``` 128 | 129 | ### Install as a PowerShell Module (Manually / Unmanaged) 130 | 131 | In order for a module to be available, it must be located in a registered module path. You can view the registered module paths by running the following command: 132 | 133 | ```powershell 134 | Write-Output $env:PSModulePath 135 | ``` 136 | 137 | Example Output: 138 | 139 | ``` 140 | C:\Users\Phrozen\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules 141 | ``` 142 | 143 | Clone PowerRemoteDesktop repository or download a Github release package. 144 | 145 | ``` 146 | git clone https://github.com/DarkCoderSc/PowerRemoteDesktop.git 147 | ``` 148 | 149 | Copy both *PowerRemoteDesktop_Viewer* and *PowerRemoteDesktop_Server* folders to desired module path 150 | 151 | Example: 152 | 153 | ``` 154 | C:\Users\\Documents\WindowsPowerShell\Modules 155 | ``` 156 | 157 | Both modules should now be available, you can verify using the command: 158 | 159 | ```powershell 160 | Get-Module -ListAvailable 161 | ``` 162 | 163 | Example Output: 164 | 165 | ``` 166 | PS C:\Users\Phrozen\Desktop> Get-Module -ListAvailable 167 | 168 | 169 | Directory: C:\Users\Phrozen\Documents\WindowsPowerShell\Modules 170 | 171 | 172 | ModuleType Version Name ExportedCommands 173 | ---------- ------- ---- ---------------- 174 | Manifest 1.0.0 PowerRemoteDesktop_Server Invoke-RemoteDesktopServer 175 | Manifest 1.0.0 PowerRemoteDesktop_Viewer Invoke-RemoteDesktopViewer 176 | 177 | <..snip..> 178 | ``` 179 | 180 | If you don't see them, run the following commands and check back. 181 | 182 | ```powershell 183 | Import-Module PowerRemoteDesktop_Server 184 | 185 | Import-Module PowerRemoteDesktop_Viewer 186 | ``` 187 | 188 | Notice: Manifest files are optional (`*.psd1`) and can be removed. 189 | 190 | ### As a PowerShell Script 191 | 192 | It is not mandatory to install this application as a PowerShell module (Even if file extension is `*.psm1`) 193 | 194 | You can also load it as a PowerShell Script. Multiple methods exists including: 195 | 196 | Invoking Commands Using: 197 | 198 | ```powershell 199 | IEX (Get-Content .\PowerRemoteDesktop_[Server/Viewer].psm1 -Raw) 200 | ``` 201 | 202 | Loading script from a remote location: 203 | 204 | ```powershell 205 | IEX (New-Object Net.WebClient).DownloadString('http://127.0.0.1/PowerRemoteDesktop_[Server/Viewer].psm1') 206 | ``` 207 | 208 | etc... 209 | 210 | ## Usage 211 | 212 | ### Client 213 | 214 | `PowerRemoteDesktop_Viewer.psm1` needs to be imported / or installed on local machine. 215 | 216 | #### Available Module Functions 217 | 218 | ```powershell 219 | Invoke-RemoteDesktopViewer 220 | Get-TrustedServers 221 | Remove-TrustedServer 222 | Clear-TrustedServers 223 | ``` 224 | 225 | #### Invoke-RemoteDesktopViewer 226 | 227 | Create a new remote desktop session with a Power Remote Desktop Server. 228 | 229 | ##### ⚙️ Supported Options: 230 | 231 | | Parameter | Type | Default | Description | 232 | |-------------------------|------------------|------------|--------------| 233 | | ServerAddress | String | 127.0.0.1 | Remote server host or address | 234 | | ServerPort | Integer | 2801 | Port number for the remote server | 235 | | SecurePassword | SecureString | None | SecureString object containing the password used for authenticating with the remote server (recommended) | 236 | | Password | String | None | Plain-text password used for authenticating with the remote server (not recommended; use SecurePassword instead)) | 237 | | DisableVerbosity | Switch | False | If specified, the program will suppress verbosity messages | 238 | | UseTLSv1_3 | Switch | False | If specified, the program will use TLS v1.3 instead of TLS v1.2 for encryption (recommended if both systems support it) | 239 | | Clipboard | Enum | Both | Specify the clipboard synchronization mode (options include 'Both', 'Disabled', 'Send', and 'Receive'; see below for more detail) | 240 | | ImageCompressionQuality | Integer (0-100) | 75 | JPEG compression level ranging from 0 (lowest quality) to 100 (highest quality) | 241 | | Resize | Switch | False | If specified, the remote desktop will be resized according to the 'ResizeRatio' option | 242 | | ResizeRatio | Integer (30-99) | 90 | Used in conjunction with the 'Resize' option, specify the resize ratio as a percentage | 243 | | AlwaysOnTop | Switch | False | If specified, the virtual desktop window will be displayed above all other windows | 244 | | PacketSize | Enum | Size9216 | Specify the network packet size for streams. Choose a size that is appropriate for your network constraints. | 245 | | BlockSize | Enum | Size64 | Specify the size of the screen grid blocks. Choose a size that is appropriate for the remote screen size and the computer's resources (such as CPU and network capabilities) | 246 | | LogonUI | Switch | False | Request the server to open the LogonUI/Winlogon desktop instead of the default user desktop (requires SYSTEM privilege in the active session) | 247 | 248 | ##### Clipboard Mode Enum Properties 249 | 250 | | Value | Description | 251 | |-------------------|----------------------------------------------------| 252 | | Disabled | Clipboard synchronization is disabled on both the viewer and server sides | 253 | | Receive | Only incoming clipboard data is allowed | 254 | | Send | Only outgoing clipboard data is allowed | 255 | | Both | Clipboard synchronization is allowed on both the viewer and server sides | 256 | 257 | ##### PacketSize Mode Enum Properties 258 | 259 | | Value | Description | 260 | |-------------------|---------------------| 261 | | Size1024 | 1024 Bytes (1KiB) | 262 | | Size2048 | 2048 Bytes (2KiB) | 263 | | Size4096 | 4096 Bytes (4KiB) | 264 | | Size8192 | 8192 Bytes (8KiB) | 265 | | Size9216 | 9216 Bytes (9KiB) | 266 | | Size12288 | 12288 Bytes (12KiB) | 267 | | Size16384 | 16384 Bytes (16KiB) | 268 | 269 | ##### BlockSize Mode Enum Properties 270 | 271 | | Value | Description | 272 | |-------------------|------------------| 273 | | Size32 | 32x32 | 274 | | Size64 | 64x64 | 275 | | Size96 | 96x96 | 276 | | Size128 | 128x128 | 277 | | Size256 | 256x256 | 278 | | Size512 | 512x512 | 279 | 280 | ##### ⚠️ Important Notices 281 | 282 | It is recommended to use SecurePassword instead of a plain-text password, even if the plain-text password is being converted to a SecureString 283 | 284 | #### Example 285 | 286 | Open a new remote desktop session to '127.0.0.1:2801' using the password 'urCompl3xP@ssw0rd' 287 | 288 | ```powershell 289 | Invoke-RemoteDesktopViewer -ServerAddress "127.0.0.1" -ServerPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) 290 | ``` 291 | 292 | #### Enumerate Trusted Servers 293 | 294 | When connecting to a new remote server for the first time, the viewer will ask if you want to trust the server's fingerprint. If you select the option to 'Always' trust this fingerprint, it will be saved in the local user registry. You can revoke the trust for this fingerprint at any time using the appropriate function. 295 | 296 | ```powershell 297 | Get-TrustedServers 298 | ``` 299 | 300 | Example output: 301 | 302 | ```` 303 | PS C:\Users\Phrozen\Desktop\Projects\PowerRemoteDesktop> Get-TrustedServers 304 | 305 | Detail Fingerprint 306 | ------ ----------- 307 | @{FirstSeen=18/01/2022 19:40:24} D9F4637463445D6BB9F3EFBF08E06BE4C27035AF 308 | @{FirstSeen=20/01/2022 15:52:33} 3FCBBFB37CF6A9C225F7F582F14AC4A4181BED53 309 | @{FirstSeen=20/01/2022 16:32:14} EA88AADA402864D1864542F7F2A3C49E56F473B0 310 | @{FirstSeen=21/01/2022 12:24:18} 3441CE337A59FC827466FC954F2530C76A3F8FE4 311 | ```` 312 | 313 | ### Permanently Delete a Trusted Server 314 | 315 | ```powershell 316 | Remove-TrustedServer -Fingerprint "" 317 | ``` 318 | 319 | ### Permanently Delete all Trusted Servers (Purge) 320 | 321 | ```powershell 322 | Clear-TrustedServers 323 | ``` 324 | 325 | --- 326 | 327 | ### Server 328 | 329 | `PowerRemoteDesktop_Server.psm1` needs to be imported / or installed on local machine. 330 | 331 | #### Available Module Functions 332 | 333 | ```powershell 334 | Invoke-RemoteDesktopServer 335 | ``` 336 | 337 | ##### ⚙️ Supported Options: 338 | 339 | | Parameter | Type | Default | Description | 340 | |------------------------|------------------|------------|--------------| 341 | | ServerAddress | String | 0.0.0.0 | IP address representing the local machine's IP address | 342 | | ServerPort | Integer | 2801 | The port number on which to listen for incoming connections | 343 | | SecurePassword | SecureString | None | SecureString object containing the password used for authenticating remote viewers (recommended) | 344 | | Password | String | None | Plain-text password used for authenticating remote viewers (not recommended; use SecurePassword instead) | 345 | | DisableVerbosity | Switch | False | If specified, the program will suppress verbosity messages | 346 | | UseTLSv1_3 | Switch | False | If specified, the program will use TLS v1.3 instead of TLS v1.2 for encryption (recommended if both systems support it) | 347 | | Clipboard | Enum | Both | Specify the clipboard synchronization mode (options include 'Both', 'Disabled', 'Send', and 'Receive'; see below for more detail) | 348 | | CertificateFile | String | None | A file containing valid certificate information (x509) that includes the private key | 349 | | EncodedCertificate | String | None | A base64-encoded representation of the entire certificate file, including the private key | 350 | | ViewOnly | Switch | False | If specified, the remote viewer will only be able to view the desktop and will not have access to the mouse or keyboard | 351 | | PreventComputerToSleep | Switch | False | If specified, this option will prevent the computer from entering sleep mode while the server is active and waiting for new connections | 352 | | CertificatePassword | SecureString | None | Specify the password used to access a password-protected x509 certificate provided by the user | 353 | 354 | ##### Server Address Examples 355 | 356 | | Value | Description | 357 | |-------------------|--------------------------------------------------------------------------| 358 | | 127.0.0.1 | Only listen for connections from the localhost (usually for debugging purposes) | 359 | | 0.0.0.0 | Listen for connections on all network interfaces, including the local network and the internet | 360 | 361 | ##### Clipboard Mode Enum Properties 362 | 363 | | Value | Description | 364 | |-------------------|----------------------------------------------------| 365 | | Disabled | Clipboard synchronization is disabled on both the viewer and server sides | 366 | | Receive | Only incoming clipboard data is allowed | 367 | | Send | Only outgoing clipboard data is allowed | 368 | | Both | Clipboard synchronization is allowed on both the viewer and server sides | 369 | 370 | ##### ⚠️ Important Notices 371 | 372 | 1. It is recommended to use SecurePassword instead of a plain-text password, even if the plain-text password is being converted to a SecureString. 373 | 2. If you do not specify a custom certificate using 'CertificateFile' or 'EncodedCertificate', a default self-signed certificate will be generated and installed on the local machine (if one does not already exist). This requires administrator privileges. To run the server with a non-privileged account, you must provide your own certificate location. 374 | 3. If you do not specify a SecurePassword or Password, a random, complex password will be generated and displayed in the terminal (this password is temporary). 375 | 376 | ##### Examples 377 | 378 | ```powershell 379 | Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) 380 | 381 | Invoke-RemoteDesktopServer -ListenAddress "0.0.0.0" -ListenPort 2801 -SecurePassword (ConvertTo-SecureString -String "urCompl3xP@ssw0rd" -AsPlainText -Force) -CertificateFile "c:\certs\phrozen.p12" 382 | ``` 383 | 384 | #### How to capture LogonUI 385 | 386 | As of version 4.0.0, it is possible to capture the LogonUI/Winlogon (UAC Prompt, Windows Login Window, CTRL+ALT+DEL, etc.). 387 | 388 | However, in order to capture the LogonUI, the server must be run under the context of 'NT AUTHORITY/System' in the current active session. 389 | 390 | There are multiple methods for spawning a process as the SYSTEM user in the active session (e.g., PsExec, Process Hacker), but for simplicity I recommend using my PowerRunAsSystem project (available on GitHub and installable through the PowerShell Gallery). 391 | 392 | ````powershell 393 | Install-Module -Name PowerRunAsSystem 394 | ```` 395 | 396 | Then run bellow command as Administrator. 397 | 398 | ```powershell 399 | Invoke-InteractiveSystemPowerShell 400 | ``` 401 | 402 | A new PowerShell terminal should appear on your desktop as **NT AUTHORITY/System** 403 | 404 | If you follow the steps above, a new PowerShell terminal should appear on your desktop running as the 'NT AUTHORITY/System' user. 405 | 406 | From this terminal, you can run the Power Remote Desktop server command and enable the 'LogonUI' option for future Power Remote Desktop viewer connections. 407 | 408 | It's worth noting that if you don't use your own X509 certificate, you will need administrator privileges to create a new server. However, you can easily create your own X509 certificate using tools such as the OpenSSL command line tool. 409 | 410 | ##### Generate your Certificate 411 | 412 | ``` 413 | openssl req -x509 -sha512 -nodes -days 365 -newkey rsa:4096 -keyout phrozen.key -out phrozen.crt 414 | ``` 415 | 416 | Then export the new certificate (**must include private key**). 417 | 418 | ``` 419 | openssl pkcs12 -export -out phrozen.p12 -inkey phrozen.key -in phrozen.crt 420 | ``` 421 | 422 | ##### Integrate to server as a file 423 | 424 | Use `CertificateFile`. Example: `c:\tlscert\phrozen.crt` 425 | 426 | ##### Integrate to server as a base64 representation 427 | 428 | Encode an existing certificate using PowerShell 429 | 430 | ```powershell 431 | [convert]::ToBase64String((Get-Content -path "c:\tlscert\phrozen.crt" -Encoding byte)) 432 | ``` 433 | or on Linux / Mac systems 434 | 435 | ``` 436 | base64 -i /tmp/phrozen.p12 437 | ``` 438 | 439 | You can then pass the output base64 certificate file to parameter `EncodedCertificate` (One line) 440 | 441 | ## Changelog 442 | 443 | ### 11 January 2022 (1.0.1 Beta 2) 444 | 445 | * Desktop images are now transported in raw bytes instead of base64 string thus slightly improving performances. 446 | * Protocol has drastically changed. It is smoother to read and less prone to errors. 447 | * TLS v1.3 option added (Might not be supported by some systems). 448 | * Several code optimization, refactoring and fixes. 449 | * Password complexity check implemented to avoid lazy passwords. 450 | * Possibility to disable verbose. 451 | * Server & Viewer version synchronization. Same version must be used between the two. 452 | 453 | ### 12 January 2022 (1.0.2 Beta 3) 454 | 455 | * HDPI is completely supported. 456 | 457 | ### 12 January 2022 (1.0.3 Beta 4) 458 | 459 | * Possibility to change desktop image quality. 460 | * Possibility to choose which screen to capture if multiple screens (Monitors) are present on remote machine. 461 | 462 | #### Multi Screen Selection 463 | 464 | ![Multi Screen Example](Assets/multi-screen.png) 465 | 466 | ### 14 January 2022 (1.0.4 Beta 5) 467 | 468 | * Password is stored as SecureString on Viewer. I don't see the point of implementing SecureString sever-side, if you do see the point, please change my mind. 469 | * Server Fingerprint Validation. 470 | * Possibility to trust a server for current PowerShell Instance or persistantly using a local storage. 471 | * Possibility to manage trusted servers (List, Remove, Remove All) 472 | 473 | #### Fingerprint Validation 474 | 475 | ![Server Fingerprint Validation](Assets/server-fingerprint-validation.png) 476 | 477 | ### 18 January 2022 (1.0.5 Beta 6) 478 | 479 | * Multiple code improvements to support incoming / outgoing events. 480 | * Global cursor state synchronization implemented (Now virtual desktop mouse cursor is the same as remote server). 481 | * Password Generator algorithm fixed. 482 | * Virtual keyboard `]` and `)` correctly sent and interpreted. 483 | * Clipboard synchronization Viewer <-> Server added. 484 | * Server support a new option to only show desktop (Mouse moves, clicks, wheel and keyboard control is disabled in this mode). 485 | 486 | ### 21 January 2022 (1.0.6) 487 | 488 | * TransportMode option removed. 489 | * Desktop streaming performance / speed increased. 490 | 491 | ### 28 January 2022 (2.0.0) 492 | 493 | * Protocol was completely revisited, protocol is now more stable and modular. 494 | * Session concurrency is now supported, multiple viewers can connect at the same time to a server. 495 | * Possibility to stop the server using CTRL+C 496 | * Image quality is now requested by viewer. 497 | * Desktop resize is now made server-side. 498 | * Desktop resize can now be forced and requested by viewer. 499 | * Center virtual desktop glitch fixed. 500 | * Handshake calls (auth + session / worker negociation) will now timeout to avoid possible dead locks. 501 | * Virtual Desktop Form can now be set always on top of other forms. 502 | * Server finally use secure string to handle password-authentication. 503 | 504 | ### 9 February 2022 (3.0.0) 505 | 506 | * Prevent computer to sleep in server side. 507 | * Motion Update now supported in its very first version to increase desktop streaming speed. 508 | * Mouse move works as expected in certain circumstances. 509 | * Keyboard simulation improved. 510 | * Various Optimization and fixes. 511 | 512 | ### 10 February 2022 (3.1.0) 513 | 514 | * Code refactoring and improvement. 515 | * Desktop streaming improvement to gain few more FPS. 516 | * Support password-protected external x509 Certificates. 517 | 518 | ### 10 March 2022 (4.0.0) 519 | 520 | * Huge desktop streaming optimization, FPS rate increased by 65% (even more if tuning BlockSize) 521 | * Desktop resize is now made viewer-side and automatically to simplify the code and efficiency. 522 | * FastResize option is not required anymore. 523 | * Various code optimization / fix. 524 | * WIN Keyboard Key supported. 525 | * Virtual Desktop window opens above the terminal. 526 | * Server now support LogonUI / Winlogon (Beta) 527 | 528 | ### List of ideas and TODO 529 | 530 | * 🟢 Mutual Authentication for SSL/TLS (Client Certificate) 531 | * 🟠 Interrupt sessions when local resolution has changed. 532 | * 🔴 LogonUI Support. 533 | 534 | 🟢 = Easy 535 | 🟠 = Medium 536 | 🔴 = Hard 537 | 538 | Made with ❤️ in 🇫🇷 539 | -------------------------------------------------------------------------------- /TestServer.ps1: -------------------------------------------------------------------------------- 1 | # cd .\Projects\PowerRemoteDesktop\; IEX (Get-Content .\TestServer.ps1 -Raw -Encoding UTF8) 2 | 3 | Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" 4 | Write-Output "⚠️ Only use this script for testing the application NOT in production ⚠️" 5 | Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" 6 | 7 | Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Server\PowerRemoteDesktop_Server.psm1" -Raw) 8 | 9 | $password = "Jade@123@Pwd" 10 | $encodedCertificate = "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" 11 | $pwdProtectedEncodedCertificate = "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" 12 | 13 | Write-Host "Scenarios" 14 | Write-Host "---------------" 15 | Write-Host "1. Classic (Secure Password)" 16 | Write-Host "2. Classic (Plain-text password)" 17 | Write-Host "3. Classic Default Certificate" 18 | Write-Host "4. TLS v1.3" 19 | Write-Host "5. Verbosity Disabled, View Only, Prevent computer to sleep" 20 | Write-Host "6. Receive clipboard only" 21 | Write-Host "7. Send clipboard only" 22 | Write-Host "8. Clipboard synchronization disabled" 23 | 24 | Write-Host "" 25 | 26 | [int]$scenario = Read-Host "Please choose scenario (default: 1)" 27 | 28 | switch ($scenario) 29 | { 30 | 2 31 | { 32 | Invoke-RemoteDesktopServer -Password $password -EncodedCertificate $pwdProtectedEncodedCertificate -CertificatePassword (ConvertTo-SecureString -String "hello" -AsPlainText -Force) 33 | } 34 | 35 | 3 36 | { 37 | Invoke-RemoteDesktopServer -Password $password 38 | } 39 | 40 | 4 41 | { 42 | Write-Host "⚡Check that TLSv1.3 is working." 43 | Write-Host "⚡Check that certificate file is correctly loaded and used." 44 | 45 | Invoke-RemoteDesktopServer -Password $password -CertificateFile "c:\temp\phrozen-pwd.pfx" -UseTLSv1_3 -CertificatePassword (ConvertTo-SecureString -String "hello" -AsPlainText -Force) 46 | } 47 | 48 | 5 49 | { 50 | Write-Host "⚡Check that verbosity is disabled." 51 | Write-Host "⚡Check that remote viewer can't control mouse and keyboard." 52 | Write-Host "⚡Check that computer wont go to sleep." 53 | 54 | Invoke-RemoteDesktopServer -Password $password -CertificateFile "c:\temp\phrozen.p12" -DisableVerbosity -ViewOnly -PreventComputerToSleep 55 | } 56 | 57 | 6 58 | { 59 | Write-Host "⚡Check if server is only authorized to receive remote clipboard." 60 | 61 | Invoke-RemoteDesktopServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Receive" 62 | } 63 | 64 | 7 65 | { 66 | Write-Host "⚡Check if server is only authorized to send local clipboard." 67 | 68 | Invoke-RemoteDesktopServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Send" 69 | } 70 | 71 | 8 72 | { 73 | Write-Host "⚡Check if clipboard synchronization is completely disabled." 74 | 75 | Invoke-RemoteDesktopServer -Password $password -EncodedCertificate $encodedCertificate -Clipboard "Disabled" 76 | } 77 | 78 | default 79 | { 80 | Invoke-RemoteDesktopServer -SecurePassword (ConvertTo-SecureString -String $password -AsPlainText -Force) -EncodedCertificate $encodedCertificate 81 | } 82 | } -------------------------------------------------------------------------------- /TestViewer.ps1: -------------------------------------------------------------------------------- 1 | # cd .\Projects\PowerRemoteDesktop\; IEX (Get-Content .\TestViewer.ps1 -Raw -Encoding UTF8) 2 | 3 | Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" 4 | Write-Output "⚠️ Only use this script for testing the application NOT in production ⚠️" 5 | Write-Output "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" 6 | 7 | Invoke-Expression -Command (Get-Content "PowerRemoteDesktop_Viewer\PowerRemoteDesktop_Viewer.psm1" -Raw) 8 | 9 | # Different Scenarios 10 | 11 | $remoteHost = "127.0.0.1" 12 | $password = "Jade@123@Pwd" 13 | 14 | Write-Host "Scenarios" 15 | Write-Host "---------------" 16 | Write-Host "1. Classic (Secure Password)" 17 | Write-Host "2. Classic (Plain-text password) + LogonUI" 18 | Write-Host "3. Always On Top, Disable Verbosity" 19 | Write-Host "4. TLS v1.3" 20 | Write-Host "5. Clipboard Receive" 21 | Write-Host "6. Clipboard Send" 22 | Write-Host "7. Clipboard Disabled" 23 | Write-Host "8. Image Quality Really Bad" 24 | Write-Host "9. Image Quality Bad" 25 | Write-Host "10. Image Quality High" 26 | Write-Host "11. Resize 10%" 27 | Write-Host "12. Resize 80%, Packet Size 16KiB, BlockSize 128x128" 28 | Write-Host "13. Bad Password" 29 | 30 | Write-Host "" 31 | 32 | [int]$scenario = Read-Host "Please choose scenario (default: 1)" 33 | 34 | switch ($scenario) 35 | { 36 | 2 37 | { 38 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -LogonUI 39 | } 40 | 41 | 3 42 | { 43 | Write-Host "⚡Check that verbosity is not shown." 44 | Write-Host "⚡Check that virtual desktop form is above all windows." 45 | 46 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -DisableVerbosity -AlwaysOnTop 47 | } 48 | 49 | 4 50 | { 51 | Write-Host "⚡Check that TLSv1.3 is working." 52 | 53 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -UseTLSv1_3 54 | } 55 | 56 | 5 57 | { 58 | Write-Host "⚡Check if viewer is only authorized to receive remote clipboard." 59 | 60 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Receive" 61 | } 62 | 63 | 6 64 | { 65 | Write-Host "⚡Check if viewer is only authorized to send local clipboard." 66 | 67 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Send" 68 | } 69 | 70 | 7 71 | { 72 | Write-Host "⚡Check if clipboard synchronization is completely disabled." 73 | 74 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Clipboard "Disabled" 75 | } 76 | 77 | 8 78 | { 79 | Write-Host "⚡Check if image quality is really low." 80 | 81 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 0 82 | } 83 | 84 | 9 85 | { 86 | Write-Host "⚡Check if image quality is not really good." 87 | 88 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 30 89 | } 90 | 91 | 10 92 | { 93 | Write-Host "⚡Check if image quality is really good." 94 | 95 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -ImageCompressionQuality 100 96 | } 97 | 98 | 11 99 | { 100 | Write-Host "⚡Check if desktop image is reduced by 10%." 101 | Write-Host "⚡Check if resize quality is bad." 102 | 103 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Resize -ResizeRatio 90 104 | } 105 | 106 | 12 107 | { 108 | Write-Host "⚡Check if desktop image is reduced by 20%." 109 | Write-Host "⚡Control block size." 110 | Write-Host "⚡Control packet size." 111 | 112 | Invoke-RemoteDesktopViewer -Password $password -ServerAddress $remoteHost -Resize -ResizeRatio 80 -PacketSize "Size16384" -BlockSize "Size128" 113 | } 114 | 115 | 13 116 | { 117 | Write-Host "⚡Be sure that authentication fails with remote server." 118 | 119 | Invoke-RemoteDesktopViewer -Password "bad@Bad123!Bad" -ServerAddress $remoteHost 120 | } 121 | 122 | default 123 | { 124 | Invoke-RemoteDesktopViewer -SecurePassword (ConvertTo-SecureString -String $password -AsPlainText -Force) -ServerAddress $remoteHost 125 | } 126 | } --------------------------------------------------------------------------------