nft_util - FTP Server Framework
The NFT_UTIL package provides the basis for implementing a File Transfer
Protocol (FTP) server. The implementation of the NFT_UTIL package itself
and its companion package of command processing functions,
nft_proc.c, was based on the following Request for Comments
(available at a number of Internet sites):
- RFC 765 - File Transfer Protocol (obsolete)
- Although superseded by RFC 959, this RFC described the (defunct?) mail-related FTP commands: MAIL, MLFL, MSAM, MSOM, MSRQ, and MRCP.
- RFC 959 - File Transfer Protocol (FTP)
- The official FTP RFC, well-written, not dry.
- RFC 1123 - Requirements for Internet Hosts -- Application and Support
- This all-encompassing RFC clarified some remaining issues in the FTP standard and took into account existing practice.
as well as some empirical testing with the SunOS 4.1.3 and HP/UX 9.05 FTP servers.
An FTP server listens for and accepts network connection requests from clients who wish to transfer files. A session for a particular client begins when that client first connects to the FTP server and ends when the client is disconnected from the server; an FTP server with multiple clients would have multiple sessions active simultaneously. Associated with each session are two network connections:
Control - is the connection over which commands are sent to the FTP server and replies returned to the client. The control connection stays open for the life of the session.
Data - is a connection over which files and other data are sent and received. A new data connection is established for each data transfer (e.g., the sending of a single file) and closed when the transfer completes.
FTP commands are CR/LF-terminated, ASCII strings consisting of an upper case, 3- or 4-character keyword followed by zero or more, space-separated arguments; for example, the following command requests the retrieval of a file:
RETR thisFile
Some of the more common FTP commands are:
USER userName(Automatically issued by the FTP client)PASS password- log a user onto the FTP server's host.
CWD newDirectory(FTP Client Command:cd newDirectory)CDUP(FTP Client Command:cd ..)- change the current working directory. CDUP moves up to the parent of the current working directory.
PWD(FTP Client Command:pwd)- returns the current working directory.
NLST [directory](FTP Client Command:ls)- returns a list of the files in the specified directory (which defaults to the current working directory).
TYPE A|I(FTP Client Command:asciiorbinary)- specifies the type of data being transferred, "A" for ASCII and "I" (Image) for binary.
PORT hostAndPort(Automatically issued by the FTP client)- specifies a port on the client's host to which the FTP server should connect for data transfers. This command is usually issued anew prior to each file transfer or directory listing.
RETR fileName(FTP Client Command:get fileName)- retrieves the specified file from the FTP server's host.
STOR fileName(FTP Client Command:put fileName)- store the to-be-transferred data on the FTP server's host under the specified file name.
QUIT(FTP Client Command:byeorquit)- terminates the session.
FTP replies consist of a 3-digit, numeric status code followed by
descriptive text. The status codes are enumerated in RFC 959
(but see RFC 1123 for some updates). For example, the
RETR command typically results in two replies being returned
to the client over the control connection:
"150 Data connection opened: thisFile (98765 bytes)"
... the contents of "thisFile" are sent over the data connection ...
"226 Transfer complete: thisFile (98765 bytes)
RFCs 959 and 1123 specify what status codes should be used in reply to which commands. Although there are a few exceptions, the implementor is free to choose the format and contents of the reply text.
The NFT package provides a server implementation with a high-level means of conducting an FTP session. The server is responsible for listening for and answering a network connection request from a client:
TcpEndpoint client, server ;
...
tcpListen ("ftp", 99, &server) ;
tcpAnswer (server, -1.0, &client) ;
Once a client connection has been established, an FTP session can be created:
NftSession session ;
...
nftCreate (client, NULL, NULL, NULL, &session) ;
The server is now ready to read and process FTP commands from the client:
char *command ;
...
nftGetLine (session, &command) ;
nftEvaluate (session, command) ;
When the client connection is broken or an FTP QUIT command is received, the server should terminate the FTP session:
nftDestroy (session) ;
Putting all of the above together yields a simple - but working - FTP server:
#include <stdio.h> -- Standard I/O definitions.
#include "tcp_util.h" -- TCP/IP networking utilities.
#include "nft_util.h" -- FTP utilities.
int main (int argc, char *argv[])
{
char *command ;
TcpEndpoint client, server ;
NftSession session ;
- Listen at port 21.
tcpListen ((argc > 1) ? argv[1] : "21", 99, &server) ;
for ( ; ; ) { -- Answer next client.
tcpAnswer (server, -1.0, &client) ;
nftCreate (client, NULL, NULL, NULL, &session) ;
nftPutLine (session, "220 Service is ready.\n") ;
for ( ; ; ) { -- Service connected client.
if (nftGetLine (session, &command)) break ;
nftEvaluate (session, command) ;
if ((nftInfo (session))->logout) break ;
}
nftDestroy (session) ; -- Lost client.
}
}
The server's name is specified as the first argument on the command line
(i.e., argv[1]) and defaults to port 21. If a client
connection is broken, the server loops back to wait for the next client.
In event-driven applications (e.g., those based on the X Toolkit or the
IOX dispatcher), the socket connection
underlying an FTP session, returned by nftFd(), can be
monitored for input by your event dispatcher. Because input is buffered,
the input callback must repeatedly call nftGetLine() or
nftRead() while nftIsReadable() is true.
The NFT_UTIL package provides a means for modifying or extending the
functionality of an NFT-based FTP server; it does this by maintaining
a table that maps FTP command keywords to the C functions that process
those commands. When called to evaluate a command string,
nftEvaluate() parses the command line into an
argc/argv array of arguments. It then
looks up the command keyword (argv[0]) in the table and
calls the command processing function bound to that keyword, passing
the argc/argv array of arguments to the function.
nftCreate() initializes a session's keyword-function map with
default entries for the commands called for in the RFCs. These default entries
are defined in the
defaultCommands[] and defaultCallbacks[] arrays
below. The default command processing functions - except for those for
PASV, PORT, and QUIT - are found in the companion package,
nft_proc.c. An application can modify the processing of an
existing command by registering a new command processing function for the
command. The following example replaces the default nftRETR()
retrieval function with myRetrieve():
extern int myRetrieve (NftSession session,
int argc, const char *argv[],
void *userData) ;
...
nftRegister (session, "RETR", myRetrieve) ;
nftRegister() can also be used to add entirely new FTP commands
to the keyword-function map. When replacing an existing command's callback,
study the RFCs and the default implementation of the command so as to
ensure that your implementation covers all the bases.
Application-specific command processing functions registered with
nftRegister() and invoked by nftEvaluate()
should be declared as follows:
int myFunction (NftSession session,
int argc,
const char *argv[],
void *userData)
{
... body of function ...
}
argc is the number of arguments (plus the command keyword) in the
command string being evaluated; argv[] is an array of character
strings, each one being one of the arguments. For example, an FTP ALLO command,
"ALLO 123 456"
would be parsed into an argc/argv array as shown here:
argc = 3 argv[0] = "ALLO"
argv[1] = "123" -- File size
argv[2] = "456" -- Maximum record/page size
The command processing function is responsible for verifying the number
and validity of a command's arguments. The userData argument
is an arbitrary (void *) pointer specified when the session
was created.
A number of NFT functions are available for use in a command processing
function. A pointer to the public information in a session structure
can be obtained with a call to nftInfo():
NftSessionInfo *info = nftInfo (session) ;
nftPutLine() should be used to format and send a reply message
to the client over the session's control connection; the following example
returns a response for the PWD command:
nftPutLine (session, "257 \"%s\"\n", info->currentDirectory) ;
Commands such as RETR, STOR, and LIST must establish a separate network
connection for transferring data back and forth. A basic implementation
of the RETR command illustrates the use of the nftOpen(),
nftWrite(), and nftClose() functions to transfer data:
#include <stdio.h> -- Standard I/O definitions.
#include <string.h> -- C Library string functions.
#include "nft_util.h" -- FTP utilities.
int myRetrieve (NftSession session,
int argc,
const char *argv[],
void *userData)
{
char buffer[1024], fileName[1024] ;
FILE *file ;
int length ;
NftSessionInfo *info = nftInfo (session) ;
-- Append the file name to the current working
-- directory and open the file for reading.
strcpy (fileName, info->currentDirectory) ;
strcat (fileName, argv[1]) ;
file = fopen (fileName, "rb") ;
-- Establish a network connection with the client
-- for the purpose of transferring data.
nftOpen (session) ;
nftPutLine (session, "150 Transferring: %s\n", fileName) ;
-- Send the contents of the file to the client.
while ((length = fread (buffer, 1, sizeof buffer, file)) > 0)
nftWrite (session, length, buffer, NULL) ;
-- End the data transfer.
nftPutLine (session, "226 Transfer complete.\n") ;
nftClose (session) ;
fclose (file) ;
return (0) ;
}
The STOR command is implemented in a similar fashion, with the calls to
fread(3) and nftWrite() replaced by calls to
nftRead() and fwrite(3), respectively. The
NFT_UTIL package handles the PORT and PASV commands that precede a data
transfer command, so nftOpen() will establish the appropriate
type of data connection with the application (or its programmer) being
none the wiser.
The myRetrieve() function above does not strictly conform to
the FTP standard in that it only supports binary transfers
("TYPE I") of arbitrary files. Text files should be
transferred according to the Telnet protocol. In particular, each
end-of-line marker (e.g., "\n") in a text file must be
transmitted as a Telnet end-of-line sequence: a carriage return followed
by a line feed ("\r\n"). The FTP client and the server are
responsible for making the appropriate conversions to and from their hosts'
end-of-line conventions when sending or receiving ASCII data.
nftRead() and nftWrite() do not perform
these conversions for you. However, the CR/LF utilities (see
crlf_util.c) simplify the handling of ASCII text. Using these
conversion utilities, the myRetrieve() function needs to be
modified in only two places in order to support ASCII text transfers:
#include "crlf_util.h" -- CR/LF utilities.
int myRetrieve (...)
{
...
-- Open the file for reading.
if (info->representation[0] == 'A') -- ASCII transfer?
file = fopen (fileName, "r") ; -- Open ASCII file.
else
file = fopen (fileName, "rb") ; -- Open binary file.
...
-- Send the contents of the file to the client.
while ((length = fread (buffer, 1, (sizeof buffer)/2, file)) > 0) {
if (info->representation[0] == 'A') {
nl2crlf (buffer, length, sizeof buffer) ;
length = strlen (buffer) ;
}
if (nftWrite (session, length, buffer, NULL)) break ;
}
...
}
Within the send loop, nl2crlf() is called to convert newline
characters in the file to the carriage return/line feed sequence. Note that
only half of the buffer is used for reading, thus allowing for the worst case
scenario of a string consisting entirely of newlines being expanded to
twice its size.
nftClose() - closes a session's data connection.
nftCreate() - creates an FTP session.
nftDestroy() - deletes an FTP session.
nftEvaluate() - evaluates an FTP command.
nftFd() - returns a session's control or data socket number.
nftGetLine() - reads the next line from a session's control connection.
nftIgnoreCmd() - ignores an FTP command.
nftInfo() - returns a pointer to the session's public information.
nftIsReadable() - checks if input is waiting to be read from a session's control or data connection.
nftIsUp() - checks if a session's control or data connection is up.
nftIsWriteable() - checks if data can be written to a session's control or data connection.
nftName() - returns the name of a session's control or data connection.
nftNextCommand() - reads and evaluates the next FTP command.
nftOpen() - opens a session's data connection.
nftPASV() - processes the FTP PASV command.
nftPeer() - returns the name of the session's peer.
nftPORT() - processes the FTP PORT command.
nftPutLine() - writes a line of output to a session's control connection.
nftQUIT() - processes the FTP QUIT command.
nftRead() - reads input from a session's data connection.
nftRegister() - maps an FTP command to a user-supplied function.
nftSyntax() - returns the syntax of an FTP command.
nftWrite() - writes output to a session's data connection.
nft_util.c
nft_util.h
nftAccessCmds() - processes FTP access control commands.
nftCWD() - processes the FTP CWD command.
nftFileCmds() - processes the FTP file management commands.
nftHELP() - processes the FTP HELP command.
nftListCmds() - processes the FTP LIST and NLST commands.
nftMODE() - processes the FTP MODE command.
nftRETR() - processes the FTP RETR command.
nftServiceCmds() - processes FTP service commands.
nftSTAT() - processes the FTP STAT command.
nftStoreCmds() - processes the FTP APPE and STOR commands.
nftSTRU() - processes the FTP STRU command.
nftTYPE() - processes the FTP TYPE command.
nftUSER() - processes the FTP USER command.
nft_proc.c
nft_proc.h