Thursday, February 22, 2007

The Implementation of an FTP Server

In the previous article, we discussed RFC 959, which defines how an FTP server should work. We also walked through some of the commands that are used in this protocol. In this article, we will be creating an example FTP server in Delphi.

Required Tools

Delphi 6 or higher and Indy 10.1.5, the latest snapshot. I've only tested this application on Windows XP, so, although I do not think you will encounter any problems on most other platforms, please let me know if you do. I will try to make the application as platform agnostic as possible.

The application

To create our FTP server, we are going to use the idftpserver component available in Delphi. This component implements all the commands needed to create a viable server application. As always, I am going to implement as many commands as possible in this application, but by no means all of them. To implement an FTP server application that meets the minimum requirements you need to:

  • Be able to show all files in a given directory.
  • Change and remove a directory.
  • Upload and download files.
  • Delete and view file details.
  • Authenticate a user.

There are many more commands, but you can just about get on with implementing the minimum. To see how many commands you can implement with Indy, drop an idftpserver component on a form and look at the object inspectors events tab. Indy has done most of the interpreting of the FTP protocol, and thus makes it easy to implement the commands.

Start Delphi and create a new application. On the form add the following components:

  • One Memo
  • Three Edits
  • Two Buttons
  • Four Labels

In edit1's text property add "myusername" and on edit2's text property add "mypassword." Then from the Indy servers tab drop the idFTPServer component, and from the Indy intercepts tab drop a TidServerInterceptlogfile component; rename it to logfile1. Now we need to connect this intercept to the idftpserver component. So click on the idftpserver component, go to its intercept property on the object inspector and click on the drop down arrow. It should contain the word "logfile1." Select it and we're done.

On button1 add the caption "Connect" and on button2 add the caption "Exit." Double click on button1 and add the following code:

procedure TForm1.Button1Click(Sender: TObject); begin idftpserver1.DefaultPort:=strtoint(edit3.text); idftpserver1.Active:=true; showmessage('Connected'); end;

Then do the same for button2 but add the following code:

procedure TForm1.Button2Click(Sender: TObject); begin idftpserver1.Active:=false; close; end;

Button1 does a couple of things. It activates the ftpserver application and also sets the default port, which is number 21. The port number is very important because it is where the client application is going to try to connect to the server. Button2 just closes down the application. Your form should now look something like this:



Now that the GUI is done, let's move on to the code. Most of the procedures below give you a clue as to what the code is all about. I have commented most of the code so that it you can easily work out what is going on in a particular procedure.

The setslashes function replaces double backslashes with single backslashes and also replaces a single frontslash with a backslash. This is to make sure that any pathnames that are sent by the client FTP application are processed in the correct order. For example if the user supplies a pathname like "c:afilename" then this function will correct it to "c:afilename," which is the correct way to handle pathnames.

function setSlashes(APath:String):String; var slash:string; begin slash := StringReplace(APath, '/', '', [rfReplaceAll]); slash := StringReplace(slash, '', '', [rfReplaceAll]); Result := slash; end;

The event below gets fired when a client FTP application connects to the server. You can use this event to track what a particular client is doing. For example you can get the IP address from which the client connected and the time that the client connected. The event is entirely optional.

procedure TForm1.IdFTPServer1Connect(AContext: TIdContext); begin //Here you could take the client IP address, time that a particular //client logged in etc, for tracking purposes end;

The Vdirectory string contains the directory to which you need to change. The directory name is sent by the client FTP application.:

procedure TForm1.IdFTPServer1ChangeDirectory(ASender:
TIdFTPServerContext; var VDirectory: String); begin
ASender.CurrentDir:= VDirectory; end;

The client sends the name of the file to be deleted in the APathName variable. The procedure below basically takes that pathname and checks to see if the file exists before deleting it.

procedure TForm1.IdFTPServer1DeleteFile(ASender:TIdFTPServerContext; const APathName: String); begin if fileexists(APathName) then begin DeleteFile(APathName); end; end;

This event gets fired when the client wants to verify whether a file exists. The procedure uses the file exists() function to carry out the request:

procedure TForm1.IdFTPServer1FileExistCheck(ASender:TIdFTPServerContext; const APathName: String; var VExist: Boolean); begin if fileexists(APathName) then begin VExist:=true; end else begin VExist:=False; end; end;

procedure TForm1.IdFTPServer1GetFileDate(ASender: TIdFTPServerContext; const AFilename: String; var VFileDate: TDateTime); var fdate:tdatetime; begin //put the file date in a variable fdate:= FileAge(AFilename); if not (fdate=-1) then begin VFileDate:=fdate; end; end;

We use the FindNext and FindFirst functions to get the requested file size as below:

procedure TForm1.IdFTPServer1GetFileSize(ASender:TIdFTPServerContext; const AFilename: String; var VFileSize: Int64); Var LFile : String; rec:tsearchrec; ASize: Int64 ; begin LFile := setslashes(homedir + AFilename ); try if FindFirst(Lfile, faAnyFile, rec) = 0 then repeat Asize:=rec.Size; until FindNext(rec) <> 0; finally FindClose(rec); end; if Asize > 1 then VFileSize:= Asize else VFilesize:=0; end;

This is perhaps the most important procedure of them all. This event lists all the files in a given directory. It is from here that you can manipulate all the files on a file system. So let's step carefully through the code.

The first thing that we do is use the FindFirst/FindNext functions to run through any files that may be in a directory. So we use the path that the client sent, which is stored in the Apath variable:

SRI := FindFirst(HomeDir +APath + '*.*', faAnyFile - faHidden -
faSysFile, SR);

Then as FindFirst finds the files, we use the specially created list item type to store the different components of the files:

LFTPItem := ADirectoryListing.Add; LFTPItem.FileName := SR.Name; LFTPItem.Size := SR.Size; LFTPItem.ModifiedDate := FileDateToDateTime(SR.Time);

Then we check to see whether a directory has been found, and store the relevant file type.

if SR.Attr = faDirectory then LFTPItem.ItemType := ditDirectory else LFTPItem.ItemType := ditFile; SRI := FindNext(SR);

Then we simply close the file search operation down and set the current directory :

FindClose(SR); SetCurrentDir(HomeDir + APath + '..');

procedure TForm1.IdFTPServer1ListDirectory(ASender:TIdFTPServerContext; const APath: String; ADirectoryListing: TIdFTPListOutput;const ACmd, ASwitches: String); var LFTPItem :TIdFTPListItem; SR : TSearchRec; SRI : Integer; begin ADirectoryListing.DirFormat := doUnix; SRI := FindFirst(HomeDir + APath + '*.*',
faAnyFile - faHidden - faSysFile, SR); While SRI = 0 do begin LFTPItem := ADirectoryListing.Add; LFTPItem.FileName := SR.Name; LFTPItem.Size := SR.Size; LFTPItem.ModifiedDate := FileDateToDateTime(SR.Time); if SR.Attr = faDirectory then LFTPItem.ItemType := ditDirectory else LFTPItem.ItemType := ditFile; SRI := FindNext(SR); end; FindClose(SR); SetCurrentDir(HomeDir + APath + '..'); end;

http://www.devarticles.com/c/a/Delphi-Kylix/The-Implementation-of-an-FTP-Server/4/