├── 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 |