ScreenConnectUtils.psm1
|
# module variables $ScriptPath = Split-Path (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition -Parent $ModuleName = (Get-Item (Get-Variable MyInvocation -Scope Script).Value.Mycommand.Definition).BaseName # turn on informational messages $InformationPreference = 'Continue' # load localized language Import-LocalizedData -BindingVariable 'Messages' -FileName 'Messages' -BaseDirectory (Join-Path $ScriptPath 'lang') # load the config if ( Test-Path "$ScriptPath\DefaultConfig.psd1" ) { # configuration parameters # we have to add it in the loader script so that it's available to the dot sourced files $ConfigSplat = @{ Name = $ModuleName CompanyName = 'Brooksworks' DefaultPath = "$ScriptPath\DefaultConfig.psd1" } # create config variable # we have to add it in the loader script so that it's available to the dot sourced files $Config = Import-Configuration @ConfigSplat } # import cached data if ( Test-Path "$ScriptPath\data\*.json" ) { $Data = @{} Get-ChildItem -Path "$ScriptPath\data" -Filter '*.json' | ForEach-Object { $Data.($_.BaseName) = Get-Content $_.FullName | ConvertFrom-Json } } <# .SYNOPSIS Extracts attachments from a JNLP file. .PARAMETER Path Path to the JNLP file to search for attachments. .PARAMETER Destination Path to destination directory. #> function Expand-JnlpAttachments { param( [Parameter(Mandatory)] [ValidatePattern('\.jnlp$')] $Path ) $Path = [System.IO.FileInfo][string]( Resolve-Path $Path ) Get-Content $Path -Raw | ForEach-Object { ([xml]$_).jnlp.'application-desc'.argument } | Where-Object { $_ -match 'JNLP_ATTACHMENTS' } | ForEach-Object { $_.Split('=',2)[1].Split(';') } | Select-Object @{N='Path';E={ Join-Path $Path.Directory.FullName $_.Split(',',2)[0] }}, @{N='Base64Data';E={ $_.Split(':',2)[1] }} | ForEach-Object { $DecodedData = [System.Convert]::FromBase64String( $_.Base64Data ) $MemoryStream = New-Object System.IO.MemoryStream ( , $DecodedData ) $DeflateStream = New-Object System.IO.Compression.DeflateStream ( $MemoryStream, [System.IO.Compression.CompressionMode]::Decompress ) $ByteList = New-Object collections.generic.list[byte] while ( ( $Byte = $DeflateStream.ReadByte() ) -ne -1 ) { $ByteList.Add( $Byte) } Set-Content -Encoding Byte -Value $ByteList.ToArray() -Path $_.Path } } function New-RemoteScheduledTask { param( [Parameter(Mandatory)] [string] $Execute, [string] $Argument, [string] $WorkingDirectory, [string] $TaskName = ( 'Deployment Task ({0}) - {1}' -f $env:USERNAME, (Get-Date -f 'yyyyMMddHHmmss') ), [Parameter(Mandatory)] [string] $ComputerName, [pscredential] $Credential, [switch] $Wait ) $CredentialSplat = @{} if ( $Credential ) { $CredentialSplat.Credential = $Credential } $TaskActionSplat = @{} $TaskActionSplat.Execute = $Execute if ( $Argument ) { $TaskActionSplat.Argument = $Argument } if ( $WorkingDirectory ) { $TaskActionSplat.WorkingDirectory = $WorkingDirectory } $TaskStart = (Get-Date).AddSeconds(5) $TaskSplat = @{ Action = New-ScheduledTaskAction @TaskActionSplat Trigger = New-ScheduledTaskTrigger -Once -At $TaskStart Description = 'Task created by PowerShell' Settings = New-ScheduledTaskSettingsSet -RunOnlyIfNetworkAvailable -StartWhenAvailable -DeleteExpiredTaskAfter 5 Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest } $Task = New-ScheduledTask @TaskSplat | %{ $_.Triggers[0].EndBoundary = $TaskStart.ToUniversalTime().AddSeconds(5).ToString('u').Replace(' ', 'T'); $_ } Invoke-Command -ComputerName $ComputerName @CredentialSplat -ScriptBlock { $Using:Task | Register-ScheduledTask -TaskName $Using:TaskName | %{ Write-Verbose ( $Using:Messages.RegisteredScheduledTaskVerboseMessage -f $Using:TaskName, $env:COMPUTERNAME ) } if ( $Using:Wait ) { Start-Sleep -Seconds 5 while ( Get-ScheduledTask -TaskName $Using:TaskName -ErrorAction SilentlyContinue ) { for ( $i = 10; $i -ge 0; $i -- ) { Write-Progress -Activity $Using:Messages.WaitingForScheduledTaskCompletionProgressActivity -Status ( $Using:Messages.WaitingForScheduledTaskCompletionProgressStatus -f $Using:TaskName ) -PercentComplete ( ( 10 - $i ) / 10 * 100 ) Start-Sleep -Seconds 1 } } } } } <# .SYNOPSIS Utility function to check if host is online. #> function Test-HostConnection { [CmdletBinding()] param( [string] $ComputerName ) if ( $PSBoundParameters.Keys -notcontains 'ErrorAction' ) { $ErrorActionPreference = 'Stop' } Write-Verbose ( $Messages.CheckingHostConnectionVerboseMessage -f $ComputerName ) # verify computer is responding if ( -not( Test-Connection -ComputerName $ComputerName -Count 1 -Quiet ) ) { Write-Error ( $Messages.HostConnectionFailedError -f $ComputerName ) return $false } # check that port 445 is open $Socket= New-Object Net.Sockets.TcpClient $IAsyncResult= [IAsyncResult] $Socket.BeginConnect( $ComputerName, 445, $null, $null ) $IAsyncResult.AsyncWaitHandle.WaitOne( 500, $true ) > $null $PortOpen = $Socket.Connected $Socket.close() if ( -not $PortOpen ) { Write-Error ( $Messages.HostPortConnectionFailedError -f $ComputerName ) return $false } return $true } <# .DESCRIPTION Connect to a ScreenConnect remote support session from PowerShell. Note that you must have the Guest Session starter extension enabled. See: https://docs.connectwise.com/ConnectWise_Control_Documentation/Supported_extensions/Productivity/Guest_Session_Starter .PARAMETER ScreenConnectUri URI for ScreenConnect instance .PARAMETER SessionName What should your session be named .PARAMETER ScreenConnectPath Path to ScreenConnect files #> function Connect-SupportSession { param( [Parameter(Mandatory=$true)] [ValidatePattern('(?# must include http/https )^https?://.+')] [ValidateNotNullOrEmpty()] [string] $ScreenConnectUri, [ValidateNotNullOrEmpty()] [string] $SessionName = "PowerShell Session - $env:COMPUTERNAME", [ValidateNotNullOrEmpty()] [string] $ScreenConnectPath = ( Join-Path $env:TEMP 'ScreenConnectClient' ) ) $ErrorActionPreference = 'Stop' if ( $PSVersionTable.PSVersion.Major -lt 3 ) { throw 'Minimum supported version of PowerShell is 3.0' } $ScreenConnectPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ScreenConnectPath) if ( -not( Test-Path -Path $ScreenConnectPath -PathType Container ) ) { New-Item -Path $ScreenConnectPath -ItemType Directory -Force > $null } $ConnectionParams = @{ y = 'Guest' h = $null p = $null s = $null k = $null i = $SessionName } $InvokeWebRequestSplat = @{ Uri = '{0}/Script.ashx' -f $ScreenConnectUri.Trim('/') UseBasicParsing = $true } $ScreenConnectJS = Invoke-WebRequest @InvokeWebRequestSplat if ( $ScreenConnectJS.RawContent -match '"h":"(?<h>[^"]+)","p":(?<p>\d+),"k":"(?<k>[^"]+)"' ) { $ConnectionParams.h = $Matches.h $ConnectionParams.p = $Matches.p $ConnectionParams.k = [uri]::EscapeDataString($Matches.k) } else { Write-Error 'Could not parse connection params!' } $InvokeRestMethodSplat = @{ Method = 'Post' Uri = '{0}/App_Extensions/2d4e908b-8471-431d-b4e0-2390f43bfe67/Service.ashx/CreateGuestSupportSession' -f $ScreenConnectUri.Trim('/') Body = (ConvertTo-Json @($SessionName) -Compress) ContentType = 'application/json' } $ConnectionParams.s = Invoke-RestMethod @InvokeRestMethodSplat $ScreenConnectArguments = ( $ConnectionParams.Keys | %{ '{0}={1}' -f $_, $ConnectionParams.$_ } ) -join '&' -replace '^', '"?' -replace '$', '"' $ScreenConnectExe = Join-Path $ScreenConnectPath 'ScreenConnect.WindowsClient.exe' if ( -not (Test-Path -Path $ScreenConnectExe ) ) { $URIs = @( '{0}/Bin/ConnectWiseControl.ClientBootstrap.jnlp{1}' -f $ScreenConnectUri.Trim('/'), $ScreenConnectArguments.Trim('"') '{0}/Bin/ScreenConnect.Client.exe.jar' -f $ScreenConnectUri.Trim('/') ) $URIs | ForEach-Object {@{ Uri = $_ ; OutFile = Join-Path $ScreenConnectPath ( Split-Path -Path ( $_ -replace '\?.*' ) -Leaf ) }} | ForEach-Object { Invoke-WebRequest @_ } Add-Type -Assembly System.IO.Compression.Filesystem [System.IO.Compression.ZipFile]::ExtractToDirectory( "$ScreenConnectPath\ScreenConnect.Client.exe.jar", "$ScreenConnectPath" ) Expand-JnlpAttachments -Path "$ScreenConnectPath\ConnectWiseControl.ClientBootstrap.jnlp" } if ( Test-Path -Path $ScreenConnectExe ) { Start-Process -FilePath $ScreenConnectExe -ArgumentList $ScreenConnectArguments } else { Write-Error 'Could not locate ScreenConnect.WindowsClient.exe' } } <# .SYNOPSIS Attempts to install ScreenConnect Host Client on a remote machine. .PARAMETER Computer Computer(s) to attempt to install. .PARAMETER Credential PSCredential object to use for authentication. .PARAMETER Username The plaintext username to use for authentication. Defaults to 'Administrator'. .PARAMETER Password The plaintext password to use for authentication. .OUTPUTS No output. #> function Install-HostClient { [CmdletBinding(SupportsShouldProcess)] param( [parameter(Mandatory=$true, Position=1, ValueFromPipeline=$True)] [string[]] $Computer, [Parameter(Mandatory=$true, Position=2)] [ValidatePattern('(?# must be an EXE or MSI )\.(exe|msi)$')] [string] $Installer, [pscredential] $Credential = [pscredential]::Empty ) begin { if ( $PSBoundParameters.Keys -notcontains 'InformationAction' ) { $InformationPreference = 'Continue' } if ( $PSBoundParameters.Keys -notcontains 'ErrorAction' ) { $ErrorActionPreference = 'Stop' } Get-Command -Name PsExec.exe > $null $CredentialSplat = @{} if ( $Credential -ne [pscredential]::Empty ) { $CredentialSplat.Credential = $Credential } $InstallerPath = Resolve-Path $Installer | Get-Item $ScheduledTaskSplat = switch ( $InstallerPath.Extension ) { '.exe' {@{ Execute = Join-Path 'C:\_ScreenConnectDeployment' $InstallerPath.Name WorkingDirectory = 'C:\_ScreenConnectDeployment' }} '.msi' {@{ Execute = 'C:\Windows\System32\msiexec.exe' Argument = '/i {0} /qn' -f ( Join-Path 'C:\_ScreenConnectDeployment' $InstallerPath.Name ) WorkingDirectory = 'C:\_ScreenConnectDeployment' }} } } process { foreach ( $ComputerItem in $Computer ) { if ( -not( Test-HostConnection $ComputerItem ) ) { continue } Write-Verbose ( $Messages.MappingTemporaryDriveVerboseMessage -f "\\$ComputerItem\C$" ) New-PSDrive -Name 'RemoteComputer' -PSProvider FileSystem -Root "\\$ComputerItem\C$" @CredentialSplat > $null if ( -not( Test-Path -Path 'RemoteComputer:\_ScreenConnectDeployment' ) ) { Write-Verbose ( $Messages.CreatingDeploymentDirectoryVerboseMessage -f 'C:\_ScreenConnectDeployment' ) New-Item 'RemoteComputer:\_ScreenConnectDeployment' -ItemType Directory > $null } Write-Verbose $Messages.PushingInstallerFileVerboseMessage Copy-Item -Path $InstallerPath -Destination 'RemoteComputer:\_ScreenConnectDeployment\' -Force Write-Information ( $Messages.InvokingScreenConnectInstallerMessage -f $ComputerItem ) New-RemoteScheduledTask @ScheduledTaskSplat -ComputerName $ComputerItem @CredentialSplat -Wait Write-Verbose ( $Messages.RemovingDeploymentDirectoryVerboseMessage -f 'C:\_ScreenConnectDeployment' ) Remove-Item 'RemoteComputer:\_ScreenConnectDeployment' -Recurse -Confirm:$false -ErrorAction Continue Write-Verbose $Messages.UnMappingTemporaryDriveVerboseMessage Remove-PSDrive -Name 'RemoteComputer' Write-Information $Messages.InstallationFinishedMessage } } } # cleanup $ExecutionContext.SessionState.Module.OnRemove = {} |