ftpslib.psm1
# PowerShell FTP(S) Toolkit v1.0 # # # Copyright 2020 Rob Kalmar # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # Functions to connect to FTP(S) server. # # Tested platforms: # Windows Server 2008 R2 # Windows Server 2012 # Windows Server 2012 R2 # Windows Server 2016 # Windows Server 2019 # # Notes: # - Only supports passive mode # - Does not support TLS session resumption on the data connection. function Show-Error { param ( [parameter(Mandatory=$true)][Management.Automation.ErrorRecord]$ThrownError ) Write-Verbose -Message "[$ThrownError]" Return $False } Function Connect-FTPServer { [CmdletBinding()] param () [int]$BufferSize = 16384 $Buffer = New-Object -TypeName byte[] -ArgumentList $BufferSize Try { $This.CommandConnection = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList $This.FQDN, $This.Port } Catch { $ErrorMessage = $_.Exception.Message Write-Verbose -Message $ErrorMessage Return $False } $This.CommandConnection.ReceiveTimeout = $This.Timeout $This.CommandConnection.SendTimeout = $This.Timeout $This.CommandStream = $This.CommandConnection.GetStream() $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n").Split("`n", 2)[0] Write-Verbose -Message "[$Response]" If ($This.SSL) { $CommandBytes = [Text.Encoding]::ASCII.GetBytes("AUTH TLS`r`n") $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" If ($This.VerifyCertificate) { $This.CommandStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.CommandStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = $null) } Else { $This.CommandStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.CommandStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = { Return $True }) } $null = $This.CommandStream.AuthenticateAsClient("$($This.FQDN)") } Return $True } Function Disconnect-FTPServer { [CmdletBinding()] param () Try { $null = $This.SubmitCommand('QUIT','') $null = $This.CommandStream.Dispose() $null = $This.CommandConnection.Close() } Catch { Return $False } Return $True } Function Submit-FTPCommand { param ( [parameter(Mandatory=$true)][string]$Command, [string]$Parameter = '' ) # some FTP servers cannot handle it when we send commands too fast. $null = Start-Sleep -Milliseconds 100 [int]$BufferSize = 16384 $Buffer = New-Object -TypeName byte[] -ArgumentList $BufferSize switch ($Command) { 'MLSD' { $CommandText = "$Command $Parameter".Trim() Write-Verbose -Message "[$CommandText]" $CommandBytes = [Text.Encoding]::ASCII.GetBytes("$CommandText`r`n") $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() Try { $This.DataConnection = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList $This.DataStreamIP, $This.DataStreamPort } Catch { $ErrorMessage = $_.Exception.Message Write-Verbose -Message $ErrorMessage Return $False } $This.DataConnection.ReceiveTimeout = $This.Timeout $This.DataConnection.SendTimeout = $This.Timeout $This.DataStream = $This.DataConnection.GetStream() If ($This.SSL) { If ($This.VerifyCertificate) { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = $null) } Else { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = { Return $True }) } Try { $null = $This.DataStream.AuthenticateAsClient("$($This.FQDN)") } Catch { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response Return $False } } Else { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response If ($Response.SubString(0,1) -eq '5') { Return $False } } $NumberOfBytes = $This.DataStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") $This.CommandOutput = $Response Write-Verbose -Message "[$Response]" $null = $This.DataStream.Dispose() $null = $This.DataConnection.Close() $Response = $null while (($Response -eq $null) -OR ($Response.SubString(0,3) -ne '226')) { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" } $This.CommandOutput = $Response Return $True } 'STOR' { $FileName = Split-Path -Path $Parameter -Leaf $CommandText = "$Command $FileName".Trim() Write-Verbose -Message "[$CommandText]" $CommandBytes = [Text.Encoding]::ASCII.GetBytes("$CommandText`r`n") $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() Try { $This.DataConnection = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList $This.DataStreamIP, $This.DataStreamPort } Catch { $ErrorMessage = $_.Exception.Message Write-Verbose -Message $ErrorMessage Return $False } $This.DataConnection.ReceiveTimeout = $This.Timeout $This.DataConnection.SendTimeout = $This.Timeout $This.DataStream = $This.DataConnection.GetStream() If ($This.SSL) { If ($This.VerifyCertificate) { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = $null) } Else { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = { Return $True }) } Try { $null = $This.DataStream.AuthenticateAsClient("$($This.FQDN)") } Catch { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response Return $False } } Else { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response If ($Response.SubString(0,1) -eq '5') { Return $False } } $fileStream = [IO.File]::OpenRead($Parameter) $filesize = (Get-Item -Path $Parameter).Length $totalread = 0 $starttime = Get-Date While( $bytesRead = $fileStream.Read($Buffer,0,$BufferSize) ) { $null = $This.DataStream.Write($Buffer, 0, $bytesRead) $null = $This.DataStream.Flush() $totalread = $totalread + $bytesRead $currenttime = Get-Date $elapsedtime = $currenttime - $starttime $elapsedseconds = $elapsedtime.TotalSeconds $speed = [math]::Round($totalread / ($elapsedseconds * 1024 * 1024),3) $completed = (($totalread/$filesize)*100) Write-Progress -Activity "Uploading file $Parameter" -Status "Total average upload speed in MB/s: $speed " -CurrentOperation 'Uploading...' -PercentComplete $completed -Id 1 } Write-Verbose -Message "Completed upload of: $Parameter. Average speed: $speed MB/s." $null = $This.DataStream.Dispose() $null = $This.DataConnection.Close() $Response = $null while (($Response -eq $null) -OR ($Response.SubString(0,3) -ne '226')) { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" } $This.CommandOutput = $Response Return $True } 'RETR' { $FileName = $Parameter $CommandBytes = [Text.Encoding]::ASCII.GetBytes("SIZE $FileName`r`n") Write-Verbose -Message "[SIZE $FileName]" $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response If ($Response.SubString(0,1) -eq '5') { Return $False } $FileSize = $Response.Split(' ')[1] $CommandText = "$Command $FileName".Trim() Write-Verbose -Message "[$CommandText]" $CommandBytes = [Text.Encoding]::ASCII.GetBytes("$CommandText`r`n") $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() Try { $This.DataConnection = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList $This.DataStreamIP, $This.DataStreamPort } Catch { $ErrorMessage = $_.Exception.Message Write-Verbose -Message $ErrorMessage Return $False } $This.DataConnection.ReceiveTimeout = $This.Timeout $This.DataConnection.SendTimeout = $This.Timeout $This.DataStream = $This.DataConnection.GetStream() If ($This.SSL) { If ($This.VerifyCertificate) { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = $null) } Else { $This.DataStream = New-Object -TypeName System.Net.Security.SslStream -ArgumentList $This.DataStream, $True, ([Net.ServicePointManager]::ServerCertificateValidationCallback = { Return $True }) } Try { $null = $This.DataStream.AuthenticateAsClient("$($This.FQDN)") } Catch { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response Return $False } } Else { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response If ($Response.SubString(0,1) -eq '5') { Return $False } } $totalwrite = 0 $starttime = Get-Date $fileStream = [IO.File]::OpenWrite($($This.DownloadLocation + '\' + $Parameter)) $NumberOfBytes = $This.DataStream.Read($Buffer, 0, $Buffer.Length) While ($NumberOfBytes -gt 0) { $totalwrite = $totalwrite + $NumberOfBytes $currenttime = Get-Date $elapsedtime = $currenttime - $starttime $elapsedseconds = $elapsedtime.TotalSeconds $speed = [math]::Round($totalwrite / ($elapsedseconds * 1024 * 1024),3) $completed = (($totalwrite/$filesize)*100) Write-Progress -Activity "Downloading file $FileName" -Status "Total average upload speed in MB/s: $speed " -CurrentOperation 'Downloading...' -PercentComplete $completed -Id 1 $null = $fileStream.Write($Buffer, 0, $Buffer.Length) $NumberOfBytes = $This.DataStream.Read($Buffer, 0, $Buffer.Length) } $null = $fileStream.Close() Write-Verbose -Message "Completed download of: $FileName. Average speed: $speed MB/s." $null = $This.DataStream.Dispose() $null = $This.DataConnection.Close() $Response = $null while (($Response -eq $null) -OR ($Response.SubString(0,3) -ne '226')) { $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") Write-Verbose -Message "[$Response]" } $This.CommandOutput = $Response Return $True } Default { $CommandText = "$Command $Parameter".Trim() Write-Verbose -Message "[$CommandText]" $CommandBytes = [Text.Encoding]::ASCII.GetBytes("$CommandText`r`n") $null = $This.CommandStream.Write($CommandBytes,0,$CommandBytes.Length) $null = $This.CommandStream.Flush() $NumberOfBytes = $This.CommandStream.Read($Buffer, 0, $Buffer.Length) $ResponseText = [Text.Encoding]::ASCII.GetString($Buffer, 0, $NumberOfBytes) $Response = $ResponseText.ToString().Trim("`r`n") If (($Response -eq $null) -OR ($Response.SubString(0,1) -eq '5')) { Write-Verbose -Message "[$Response]" $This.CommandOutput = $Response Return $False } If ($Command -eq 'PASV') { $array = (($Response -Split '\(')[1] -split '\)')[0] -split ',' $This.DataStreamPort = ([int]$array[4] * 256) + [int]$array[5] $This.DataStreamIP = $($array[0] + '.' + $array[1] + '.' + $array[2] + '.' + $array[3]) } $This.CommandOutput = $Response Write-Verbose -Message "[$Response]" Return $True } } } Function Invoke-Authentication { [CmdletBinding()] param ( [string]$Username = $This.Username, [string]$Password = $This.Password ) If ($This.SSL) { Try { ( $This.SubmitCommand('USER',$Username) -and $This.SubmitCommand('PASS',$Password) -and $This.SubmitCommand('PBSZ','0') -and $This.SubmitCommand('PROT','P') ) } Catch { Return $False } } Else { Try { ( $This.SubmitCommand('USER',$Username) -and $This.SubmitCommand('PASS',$Password) ) } Catch { Return $False } } } Function Invoke-ChangeDir { param ( [parameter(Mandatory=$true)][string]$Directory ) Try { ( $This.SubmitCommand('TYPE','A') -and $This.SubmitCommand('CWD',"$Directory") ) } Catch { Return $False } } Function Invoke-MakeDir { param ( [parameter(Mandatory=$true)][string]$Directory ) Try { ( $This.SubmitCommand('TYPE','A') -and $This.SubmitCommand('MKD',"$Directory") ) } Catch { Return $False } } Function Invoke-ListDir { [CmdletBinding()] param () Try { ( $This.SubmitCommand('TYPE','A') -and $This.SubmitCommand('PASV','') -and $This.SubmitCommand('MLSD','') ) } Catch { Return $False } } Function Invoke-UploadFile { param ( [parameter(Mandatory=$true)][string]$FilePath ) Try { ( $This.SubmitCommand('TYPE','I') -and $This.SubmitCommand('PASV','') -and $This.SubmitCommand('STOR',"$FilePath") ) } Catch { Return $False } } Function Invoke-DownloadFile { param ( [parameter(Mandatory=$true)][string]$FileName ) Try { ( $This.SubmitCommand('TYPE','I') -and $This.SubmitCommand('PASV','') -and $This.SubmitCommand('RETR',"$FileName") ) } Catch { Return $False } } Function Invoke-DeleteFile { param ( [parameter(Mandatory=$true)][string]$FileName ) Try { ( $This.SubmitCommand('TYPE','A') -and $This.SubmitCommand('DELE',"$FileName") ) } Catch { Return $False } } Function New-FTPObject { param ( [parameter(Mandatory=$true)][string]$FQDN, [string]$Port = '21', [string]$Username = '', [string]$Password = '', [int]$Timeout = 10000, [bool]$SSL = $False, [bool]$VerifyCertificate = $False ) Write-Verbose -Message 'Creating new FTP Server object.' $FTPServer = new-object -TypeName PSObject $FTPServer | Add-Member -Name FQDN -Value $FQDN -MemberType NoteProperty $FTPServer | Add-Member -Name Port -Value $Port -MemberType NoteProperty $FTPServer | Add-Member -Name Username -Value $Username -MemberType NoteProperty $FTPServer | Add-Member -Name Password -Value $Password -MemberType NoteProperty $FTPServer | Add-Member -Name Timeout -Value $Timeout -MemberType NoteProperty $FTPServer | Add-Member -Name SSL -Value $SSL -MemberType NoteProperty $FTPServer | Add-Member -Name VerifyCertificate -Value $VerifyCertificate -MemberType NoteProperty $FTPServer | Add-Member -Name DownloadLocation -Value $($(Split-Path -Path $script:MyInvocation.MyCommand.Path) + '\download') -MemberType NoteProperty $FTPServer | Add-Member -Name CommandConnection -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name DataConnection -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name CommandStream -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name DataStream -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name DataStreamIP -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name DataStreamPort -Value $null -MemberType NoteProperty $FTPServer | Add-Member -Name CommandOutput -Value $null -MemberType NoteProperty $guid = [guid]::NewGuid() $FTPServer | Add-Member -Name guid -Value $guid -MemberType NoteProperty $FTPServer | Add-Member -MemberType ScriptMethod -Name Connect -Force -Value { [CmdletBinding()] param () Try { $Result = Connect-FTPServer -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name Close -Force -Value { [CmdletBinding()] param () Try { $Result = Disconnect-FTPServer -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name SubmitCommand -Force -Value { [CmdletBinding()] param ( [string]$Command = '', [string]$Parameter = '' ) Try { $Result = Submit-FTPCommand -Command $Command -Parameter $Parameter -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name Authenticate -Force -Value { [CmdletBinding()] param ( [string]$Username = $This.Username, [string]$Password = $This.Password ) Try { $Authenticated = Invoke-Authentication -Username $Username -Password $Password -Verbose } Catch { $Authenticated = Show-Error -ThrownError $_ -Verbose } Return $Authenticated } $FTPServer | Add-Member -MemberType ScriptMethod -Name ChangeDir -Force -Value { param ( [parameter(Mandatory=$true)][string]$Directory ) Try { $Result = Invoke-ChangeDir -Directory $Directory -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name MakeDir -Force -Value { param ( [parameter(Mandatory=$true)][string]$Directory ) Try { $Result = Invoke-MakeDir -Directory $Directory -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name ListDir -Force -Value { [CmdletBinding()] param () Try { $Result = Invoke-ListDir -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name UploadFile -Force -Value { param ( [parameter(Mandatory=$true)][string]$FilePath ) Try { $Result = Invoke-UploadFile -FilePath $FilePath -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name DownloadFile -Force -Value { param ( [parameter(Mandatory=$true)][string]$FileName ) Try { $Result = Invoke-DownloadFile -FileName $FileName -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } $FTPServer | Add-Member -MemberType ScriptMethod -Name DeleteFile -Force -Value { param ( [parameter(Mandatory=$true)][string]$FileName ) Try { $Result = Invoke-DeleteFile -FileName $FileName -Verbose } Catch { $Result = Show-Error -ThrownError $_ -Verbose } Return $Result } Return $FTPServer } Export-ModuleMember -Function 'New-FTPObject' |