File Transfer Protocol (FTP)

Another popular method for transferring files over the Internet is the File Transfer Protocol (FTP). Unlike HTTP, an application that uses FTP will typically connect to a server and remain connected while it transfers multiple files back and forth. Even though the last few years have seen a significant number of file transfer operations moving toward using HTTP and GET requests, FTP still remains a popular protocol, as it provides good access control and a reliable file transfer mechanism. Pocket PC supports FTP through many of the same WinInet API functions you have already seen (such as InternetReadFile()), with the addition of a few FTP-specific commands.

WARNING:

Be aware of a bug that currently exists in Pocket PC regarding FTP connections and user authorization. Currently, WinInet's FTP support always uses "anonymous" for the username and password, regardless of what you set them to, making FTP unusable on servers that require authorization. This problem has been fixed in the latest service pack to Pocket PC. Please visit http://www.microsoft.com/pocketpc for more information.


The FTP Protocol

Before we dive into how you can use WinInet to perform FTP-based transfers, let's take a quick look at the FTP protocol itself (see Figure 2.4).

Figure 2.4. FTP protocol

graphics/02fig04.gif

FTP is unlike other protocols we've seen so far in that it actually uses two TCP connections to communicate to the server and transfer files. The first connection, which is known as the control connection, is established when a client connects to an FTP server (its well-known port is 21). Once you have connected, the client application uses the control connection to communicate back and forth with the FTP server, sending commands and receiving responses much in the same way HTTP does. This may include operations such as navigating directories and removing files.

Once the client finds a file to transfer, you need to establish your second connection, which is known as the data connection. The data connection is unlike others you've seen so far?the server initiates the connection by contacting the client on a specified port. This happens when a client sends the server the PORT command (which contains the client's IP address and an available port), which is followed by the actual file request (using the RETR command). Once a connection from the server to the client has been accepted, the server transfers the file to the client. Once the transfer is complete, the data connection is closed; however, the control connection remains active throughout the process until you disconnect from the FTP server or a timeout occurs.

If you are concerned about security, or are running your device inside a firewall or NAT server, the FTP protocol also supports the concept of a passive (or PASV) data connection. Instead of sending the server a port that is currently available on the client to host the data connection, FTP can request a transfer that instructs the server to open an additional port for it to listen on. Using a passive FTP transfer still requires two connections; however, both are initiated from the client. You can specify that you want to be in passive mode by using the INTERNET_FLAG_PASSIVE flag when creating a new Internet connection handle.

While FTP might initially seem more complicated than HTTP, you will find that WinInet handles most of the complexities associated with the protocol for you. More detailed information about the actual FTP protocol commands can be found in RFC 959.

Establishing an FTP Connection

Before you can perform any specific WinInet FTP command, you need to have a HINTERNET handle for an active connection to your target FTP server. This can be accomplished in the same way you created a connection for HTTP. First, initiate an Internet connection using the InternetOpen() function, and then establish a connection to the FTP server using either the InternetConnect() or InternetOpenUrl() functions:

HINTERNET hInternet = NULL, hIConnect = NULL, hIUrlConnect =
  NULL;

// First, open the Internet connection
hInternet = InternetOpen(TEXT("Sample Application"),
   INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);

// Make sure there are no errors
if(!hInternet)
   return FALSE;

// Next, connect to the Internet via InternetConnect
hIConnect = InternetConnect(hInternet,
   TEXT("ftp.microsoft.com"), INTERNET_DEFAULT_FTP_PORT,
   NULL, NULL, INTERNET_SERVICE_FTP, INTERNET_FLAG_PASSIVE, 0);

// Finally, close the handles
InternetCloseHandle(hIConnect);

// -OR- Connect to the Internet via InternetOpenUrl
hIUrlConnect = InternetOpenUrl(hInternet,
   TEXT("ftp://ftp.microsoft.com/"), NULL, 0, INTERNET_FLAG_PASSIVE, 0);

// Finally, close the handles
InternetCloseHandle(hIUrlConnect);
InternetCloseHandle(hInternet);

As with HTTP, any HINTERNET handle that you have opened must be closed using the InternetCloseHandle() function. Notice that in both instances of connecting to an FTP server, you use the INTERNET_FLAG_PASSIVE flag to create a passive FTP session.

After you have established a connection to the FTP server, you can proceed to navigate around and transfer files using the WinInet FTP APIs.

FTP Navigation and Manipulation

Because the files located on an FTP server are stored in a directory structure (similar to the Pocket PC object store or the Win32 file system), several available functions will simplify finding files, navigating directories, and deleting files.

Once you have established an FTP connection, you can enumerate the files that are located in a remote FTP directory by using the FtpFindFirstFile() and InternetFindNext() file functions. The FtpFindFirstFile() function is similar to the other enumeration functions we've already examined, such as FindFirstUrlCacheEntry() or FindFirstFile(), which enable you to "walk" the files in a particular directory that match a specific pattern. After calling the first function, you receive a handle that you can keep calling to get additional file information until there are no more to enumerate.

Here is the definition for FtpFindFirstFile():

HINTERNET FtpFindFirstFile(HINTERNET hConnect, LPCWSTR
  lpszSearchFile, LPWIN32_FIND_DATA lpFindFileData, DWORD dwFlags,
   DWORD dwContext);

The first parameter is the handle to your active FTP connection, followed by a string that contains the filename or path you want to search. You can always pass a NULL value into lpszSearchFile if you want to return all files in the current directory. The next parameter, lpFindFileData, is a pointer to the standard Windows WIN32_FIND_DATA structure, which will receive information about the file. The dwFlags parameter is used to control how data is received from the server, and can be one of the flags in Table 2.11.

Table 2.11. FTP Control Flags

Flag

Description

INTERNET_FLAG_DONT_CACHE

Do not cache any data.

INTERNET_FLAG_HYPERLINK

Force a reload of the data.

INTERNET_FLAG_MUST_CACHE_REQUEST

Create a temporary file if the file cannot be cached.

INTERNET_FLAG_NEED_FILE

Same as INTERNET_FLAG_MUST_CACHE_REQUEST.

INTERNET_FLAG_RELOAD

Reload data from the wire.

INTERNET_FLAG_RESYNCHRONIZE

Reload FTP data if the resource was modified since the last time it was requested.

The final parameter, dwContext, is an application-defined value that you can use to send your callback routine if you created one using the InternetSetStatusCallback() function. You cannot create multiple file enumerations at the same time over FTP. Once you call FtpFindFirstFile(), you must close the enumerator with InternetCloseHandle() before you can create a new instance of FtpFindFirstFile(), or else the function will fail.

Now that you have created your enumerator, you can list additional files that match the pattern specified by calling the InternetFindNextFile() function:

BOOL InternetFindNextFile(HINTERNET hFind, LPVOID lpvFindData);

This function takes only two parameters: hFind, which is the handle you were returned from FtpFindFirstFile(), and a pointer to a WIN32_FIND_DATA structure to receive additional file information.

Once you have finished with your enumeration, you need to call InternetCloseHandle() to properly close it. For example, if you wanted to output all of the files located in the root directory of an FTP site, you could simply do the following:

HINTERNET hInternetFind = NULL;
WIN32_FIND_DATA w32FindData;
BOOL fContinue = TRUE;
memset(&w32FindData, 0, sizeof(WIN32_FIND_DATA));

hInternetFind = FtpFindFirstFile(hIConnect, NULL,
  &w32FindData, 0, 0);

do {
   // Do something with the file information here

   // Get the next file
   memset(&w32FindData, 0, sizeof(WIN32_FIND_DATA));
   fContinue = InternetFindNextFile(hInternetFind,
      &w32FindData);
} while(fContinue);

// Close the find enumerator
InternetCloseHandle(hInternetFind);

If you need to directly navigate to a directory or want to find out where you are in the FTP directory tree, you can simply use the FtpSetCurrentDirectory() and FtpGetCurrentDirectory() functions to set or retrieve information about the current path:

BOOL FtpSetCurrentDirectory(HINTERNET hConnect, LPCWSTR
  lpszDirectory);

BOOL FtpGetCurrentDirectory(HINTERNET hConnect,
   LPWSTR lpszCurrentDirectory, LPDWORD lpdwCurrentDirectory);

The FtpSetCurrentDirectory() function simply takes the current FTP connection handle as the first parameter, followed by the directory to which you want to switch.

To use the FtpGetCurrentDirectory() function, pass the connection handle along with a buffer to receive the directory name. The last parameter, lpdwCurrentDirectory, will receive a pointer to the size of the buffer that was copied into lpszCurrentDirectory.

Finally, using FTP also enables you to manipulate directories by either creating or removing them (as long as your current authorization level permits it). To remove a directory, call the FtpRemoveDirectory() function:

BOOL FtpRemoveDirectory(HINTERNET hConnect, LPCWSTR lpszDirectory);

Once again, you will pass in the active FTP connection handle as the first parameter, followed by a buffer containing the name of the directory you want to delete. Conversely, if you want to create a new directory, just call FtpCreateDirectory(), which is defined as follows:

BOOL FtpCreateDirectory (HINTERNET hConnect, LPCWSTR lpszDirectory);

The FtpCreateDirectory() function takes the same parameters as FtpRemoveDirectory().

Working with Files

Now that you've learned how to explore what is on an FTP server, as well as navigate around, let's take a look at what FTP is designed to do?transfer files.

Downloading a file from an FTP server is similar to what you saw with HTTP?you first need to open the file, and then use the InternetReadFile() function to read the data for the transfer. To open an FTP file request, you can use the FtpOpenFile() function:

HINTERNET WINAPI FtpOpenFile(HINTERNET hConnect, LPCWSTR
  lpszFileName, DWORD dwAccess, DWORD dwFlags, DWORD dwContext);

After passing in the handle to your active FTP connection, you use the lpszFileName parameter to pass in the name of the file you want to download. The next parameter, dwAccess, specifies how you are accessing the remote file, and can be set to either the GENERIC_READ or GENERIC_WRITE flag. The dwFlags parameter is used to set specifics of the file download, and can be a combination of the flags in Table 2.12.

Table 2.12. FTP File Download Flags

Flag

Description

FTP_TRANSFER_TYPE_ASCII

Transfer the file as an ASCII file.

FTP_TRANSFER_TYPE_BINARY

Transfer the file as a binary file (default).

FTP_TRANSFER_TYPE_UNKNOWN

Same as FTP_TRANSFER_TYPE_BINARY.

INTERNET_FLAG_TRANSFER_ASCII

Same as FTP_TRANSFER_TYPE_ASCII.

INTERNET_FLAG_TRANSFER_BINARY

Same as FTP_TRANSFER_TYPE_BINARY.

INTERNET_FLAG_DONT_CACHE

Do not cache any data.

INTERNET_FLAG_HYPERLINK

Force a reload of the data.

INTERNET_FLAG_MUST_CACHE_REQUEST

Create a temporary file if the file cannot be cached.

INTERNET_FLAG_NEED_FILE

Same as INTERNET_FLAG_MUST_CACHE_REQUEST.

INTERNET_FLAG_RELOAD

Reload data from the wire.

INTERNET_FLAG_RESYNCHRONIZE

Reload FTP data if the resource was modified since the last time it was requested.

The final parameter, dwContext, can be set to a DWORD value that will be passed to a callback function if you have set one up using the InternetSetStatusCallback() API. Be aware that once FtpOpenFile() has been called, all other FTP functions that use the same FTP connection handle will fail until you have called InternetCloseHandle() on the file handle returned from the original call to FtpOpenFile().

The following example downloads a file using the connection previously established:

// Download a file
HINTERNET hTransfer = NULL;

LPVOID lpBuffer = NULL;
DWORD dwRead = 0, dwBytesAvailable = 0;
BOOL bActive = TRUE, fSuccess = FALSE;
HANDLE hFileDownload = NULL;
TCHAR tchName[MAX_PATH] = TEXT("\\MISC\\INDEX.TXT");

// Open the transfer
hTransfer = FtpOpenFile(hIConnect, tchName, GENERIC_READ,
   FTP_TRANSFER_TYPE_ASCII, 0);

// Ok, the file transfer is active. Create a new file to write to
hFileDownload = CreateFile(TEXT("\\index.txt"), GENERIC_WRITE, 0,
   NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
lpBuffer = (LPVOID)LocalAlloc(LPTR, 4096);

if(!lpBuffer) {
   // Close the handles
   InternetCloseHandle(hTransfer);
   InternetCloseHandle(hIConnect);
   InternetCloseHandle(hInternet);
   return FALSE;
}

// Looks like everything's ok, so use InternetReadFile to download
// the file
do{
   fSuccess = InternetReadFile(hTransfer, lpBuffer, 4096,
      &dwRead);

   if(!fSuccess || dwRead == 0) {
      bActive = FALSE;
      break;
   }

   // Write out the buffer, and get more data
   DWORD dwWritten = 0;
   WriteFile(hFileDownload, lpBuffer, dwRead, &dwWritten, NULL);

   // Clear the buffer
   memset(lpBuffer, 0, 4096);
   dwRead = 0;
} while(bActive);

LocalFree(lpBuffer);
CloseHandle(hFileDownload);
InternetCloseHandle(hTransfer);

Sending a file to an FTP server is essentially the same operation as downloading a file: First open a file connection handle using FtpOpenFile(), and then write the data in "blocks" to the server using the InternetWriteFile() function. When you call FtpOpenFile() to send a file, be sure to specify GENERIC_WRITE for the dwAccess parameter. InternetWriteFile() is defined as follows:

BOOL InternetWriteFile(HINTERNET hFile, LPCVOID lpBuffer,
   DWORD dwNumberOfBytesToWrite, LPDWORD lpdwNumberOfBytesWritten);

To use InternetWriteFile(), you will need to pass in the HINTERNET handle you received from the original call to FtpOpenFile() in the hFile parameter. The lpBuffer parameter contains a pointer to the buffer of data that you want to send to the server, whose size is specified by a DWORD that you place in dwNumberOfBytesToWrite. Finally, lpdwNumberOfBytesWritten returns a pointer to a DWORD value that specifies the number of bytes that were sent to the server.

Sending a file to an FTP server then would look something like the following:

// Upload a file
HINTERNET hTransfer = NULL;
LPVOID lpBuffer = NULL;
DWORD dwRead = 0, dwBytesAvailable = 0;
BOOL bActive = TRUE, fSuccess = FALSE;
DWORD HANDLE hFileUpload = NULL;
TCHAR tchName[MAX_PATH] = TEXT("\\Misc\\UploadName.txt");

// Open the transfer
hTransfer = FtpOpenFile(hIConnect, tchName, GENERIC_WRITE,
   FTP_TRANSFER_TYPE_ASCII, 0);

// Ok, the file transfer is active. Open the local file
hFileUpload = CreateFile(TEXT("\\UploadName.txt"),
   GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
      NULL);
lpBuffer = (LPVOID)LocalAlloc(LPTR, 4096);

if(!lpBuffer) {
   // Close the handles
   InternetCloseHandle(hTransfer);
   InternetCloseHandle(hIConnect);
   InternetCloseHandle(hInternet);
   return FALSE;
}

// Looks like everything's ok, so use InternetWriteFile to upload
// the file
do{
   DWORD dwWritten = 0;

   // Read into the buffer from the local file
   ReadFile(hFileUpload, lpBuffer, 4096, &dwRead, NULL);

   // Send the buffer
   fSuccess = InternetWriteFile(hTransfer, lpBuffer, dwRead,
      &dwWritten);

   if(!fSuccess || dwRead == 0) {
      bActive = FALSE;
      break;
   }

   memset(lpBuffer, 0, 4096);
   dwRead = 0;
} while(bActive);

LocalFree(lpBuffer);

CloseHandle(hFileUpload);
InternetCloseHandle(hTransfer);

Finally, the last piece of functionality to examine is the ability to rename and delete files on the server. To delete a file, you use the FtpDeleteFile() function:

BOOL FtpDeleteFile(HINTERNET hConnect, LPCWSTR
  lpszFileName);

The first parameter, hConnect, is the typical FTP connection handle, and lpszFileName is the name and path of the file you want to delete on the remote server. If the file is successfully deleted, the function will return TRUE; otherwise, it will return FALSE.

Finally, if you want to rename a file, you can use the following function:

BOOL FtpRenameFile(HINTERNET hConnect, LPCWSTR lpszExisting,
   LPCWSTR lpszNew);

The FtpRenameFile() function also requires an active FTP connection handle. This is followed by the lpszExisting parameter, which specifies the current filename, and the lpszNew parameter, which contains a string buffer for the new name for the file.

Remember that both the FtpDeleteFile() and FtpRenameFile() functions will fail if you do not have sufficient access on the FTP server.