Monday, November 1, 2010

Download FTP Files Using FTP Over SSL (SFTP) in .NET

Recently with the help of some folks over at StackOverflow I got pointed in the right direction for working with files on a FTP server and communicating with it via FTP over SSL. I had success using FTP client software to view and download files, but needed the code to do this programmatically in .NET. It is essentially easier than I thought and just casts a 'WebRequest' object to an 'FTPWebRequest' object and then the methods called are identical to those used in a traditional WebRequest call.

The main 3 obstacles I ran into where the following:

- Getting the Object parameter values set properly to match those of the client software I was already having success connecting with.
- Validating the Server's SSL Certificate programmatically.
- Getting around a 550 error I kept receiving when accessing the file directly.

So let’s get right to all of the code:

'Using an anonymous method, check to make sure the SSL Certificate being served up is the correct one.
'This method of inlining the validation is good for a simple single certificate check; for more involved
'checking you will want to use the line below and long hand the method call.
ServicePointManager.ServerCertificateValidationCallback = Function(obj As [Object], certificate As X509Certificate, chain As X509Chain, errors As SslPolicyErrors) (certificate.Subject.Contains("CN=ftp.domain.com"))
'Wire up a method that will be called upon creating the FTtpWebRequest and will validate the SSL Certificate
'ServicePointManager.ServerCertificateValidationCallback = New System.Net.Security.RemoteCertificateValidationCallback(AddressOf CertificateValidation)

'Create an FTPWebRequest providing the URI of the FTP server to connect
Dim Request As System.Net.FtpWebRequest = DirectCast(WebRequest.Create(New Uri("ftp://sftp.domain.com/MyFiles/Folder1/MyFile.txt")), System.Net.FtpWebRequest)
'Set that we will be downloading a file
Request.Method = WebRequestMethods.Ftp.DownloadFile
'We are going to enable SSL for the communication with the FTP server as required by the remote server.
Request.EnableSsl = True
'The credentials needed to log onto the server
Request.Credentials = New Net.NetworkCredential("UserName", "Password")
'Use a 'Passive' data transfer process. This setting was the same in my FTP client software.
Request.UsePassive = True
'Create a 'Reponse object getting the downloaded file
Dim Response As System.Net.FtpWebResponse = DirectCast(Request.GetResponse(), System.Net.FtpWebResponse)

'Read the File using a StreamReader:
Dim sr As New StreamReader(Response.GetResponseStream())
To solve my 1st issue I mentioned (get object parameters set), I essentially found and configured the FTPWebRequest object to mimic an existing workable connection from my client software. A lot of the FTP 'lingo/jargon' is identical between .NET and FTP client software. I recommend connecting to the FTP server as I did 1st using client software (i.e. FileZilla) to make sure you do have everything correct before running in circles with code that would never work because you don’t have permissions anyway.

Next I had to validate the servers SSL certificate programmatically. I was able to use a Lambda Expression using an anonymous method to to create a delegate that would check to make sure the proper SSL Certificate was being presented. If the validation logic is more elaborate, you can use the commented out call to wire up an event to a method named 'CertificateValidation()' by assigning the ServerCertificateValidationCallback on the ServicePointManager object.

Private Shared Function CertificateValidation(ByVal sender As Object, ByVal cert As X509Certificate, ByVal chain As X509Chain, ByVal [error] As System.Net.Security.SslPolicyErrors) As Boolean

'Make sure the correct certificate is being used:
If cert.Subject.Contains("CN=ftp.domain.com") Then
Return True
Else
Return False
End If

End Function
Now either method could technically just return 'True' and all certificates would be trusted and validated, but I wanted to make sure I am actually presented with the correct certificate. You can easily find out the name of the certificate by turning on tracing in the web.config (explained below). Then you can make sure the proper certificate was served, and then Return True. This process actually exists the 1st time you probably connected to the FTP server with the FTP client software (i.e. FileZilla). A dialog probably presented itself asking if you trusted the certificate. This code is dealing with that process programmatically.

The last fix was in regards to solving this specific issue:

"The remote server returned an error: (550) File unavailable (e.g., file not found, no access)."

Now this was probably an issue specific to my setup, but I mention it because the oversight is probably common. I received this upon getting the FtpWebResponse object. The issue stemmed from the URI I was providing. I knew the URI had to be the full path to the file I wanted to download, so I used ftp://sftp.domain.com/MyFiles/Folder1/MyFile.txt I tried in a browser and had the same issue. I ended up turning on a listener to log System.Net traffic. The following article has directions on doing this: Using System.Net Tracing This output (found in the root folder in my project after running) showed that the default path was already /MyFiles/Folder1. This was already the path as soon as I connect to the base URI of sftp.domain.com, and I should have recognized this from the FTP client software, as I was directly taken to this folder location each time I connected. Therefore, when I fully qualified the URI, it actually resulted in looking for the following: ftp://sftp.domain.com/MyFiles/Folder1/MyFiles/Folder1/MyFile.txt This showed me that all I needed to do was just use the host + filename like the following which worked perfectly: ftp://sftp.domain.com/MyFile.txt

This code demonstrated how to download a file from a FTP server using SSL, but there are many other operations you can do as well (i.e. Uploading, file renaming, directory listing, etc.). Just modify the Enumeration value of 'Request.Method'.

2 comments:

  1. This is really good, but from this I could not how host WCF REST service over HTTP and HTTPS with SSL running. Example: Access GET over http and POST over HTTPS with SSL running.

    ReplyDelete
  2. *PLEASE* always include your IMPORTS statements.
    Without them... none of your code does anything.
    Ugh.

    ReplyDelete