DELPHI AND THE INTERNET

by Charlie Calvert


This paper focuses on using Delphi with the Internet. There are two specific technologies examined in this paper:

One of the big developments in the computer world this spring has been the rolling out of the Microsoft Internet strategy. The days of CGI and third parties supplying even the most basic Internet tools can at last be put behind us. There will always be a demand for complex tools from third parties, but now programmers will find many of the basic Internet tools they need built into the operating system. In short, with no further investments, you can use existing free Delphi resources to:

Since Delphi can call the entire Windows API, and since we have support for OCX/ActiveX, this new strategy from Microsoft fits in very neatly with our plans. They can build the tools, and Delphi programmers can reap the harvest!


WHAT'S IN THIS PAPER?

This paper contains three big sections, and a number of smaller sections. The big break down into major topics is as follows:

  1. Finding Materials: Where to get the files you need to use the technologies described in this paper. Also included is a brief section on the hardware and software you need to run this code
  2. ISAPI: How to use ISAPI.
  3. WININET: How to use WININET.

In general, the WININET and ISAPI sections of this paper are entirely independent, and you can read them in which ever order you like.


FINDING MATERIALS, HARDWARE, SOFTWARE REQUIRMENTS

You will need a copy of Microsoft Windows NT 3.51 Server or NT 4.0 Server with the Internet Information Server loaded in order to use the technology in this paper. The Internet Information Server ships with NT Server 4.0, and at the time of this writing users of NT 3.51 can download it from the Microsoft WEB site. In general, you need a 486 computer with 20 or more megs of RAM to use Windows NT.

You should also have a second computer equipped with a WEB browser. For the ISAPI sections of this paper, this second computer can be running any software that supports WEB browsing. The WININET code will work best if you have a copy of Windows 95 or Windows NT running on your system. Any reasonable WEB browser can be used with all of this technology.

The Delphi 2.0 update, released in June of 1996, has most of the materials you need to get Delphi connected to the Internet. If you don't have this update, or if you need to find a particular file mentioned in this paper, everything that you need is also available for free from the WEB. All of the technologies discussed in this paper work with Delphi 2.0, but do not necessarily work with 16 bit Delphi.

If you need to download information from the WEB, here's where to go to get started:

http://www.borland.com/TechInfo/delphi/index.html

New versions of Delphi 2.0 ship with WININET.PAS. However, if your copy doesn't have it, then this Borland WEB page can supply it for you. WININET.PAS contains manifests, functions, types and prototypes for the Microsoft Windows Internet Extensions. This means you can now easily add FTP, Gopher and HTTP support to your programs. Microsoft's WININET.DLL is freely distributable, and is available from Microsoft if it is not already installed in your Windows/System or Windows/System32 directory. Here is how to get the Windows help file for WININET.H:

http://www.microsoft.com/intdev/sdk/docs/wininet/default.htm

In general, it's the INTDEV section of the Microsoft WEB site that is home ground for MS Internet developers.

Besides WININET and ICP another key technology supported by Delphi is ISAPI. As stated in the Microsoft documentation, this technology allows you to "Write server-side scripts and filters to extend the capabilities of Microsoft Internet Information Server and other ISAPI Web servers." Here's where to go if you want to find out about the ISAPI specification:

http://www.microsoft.com/intdev/sdk/servapi.htm

A copy of the key ISAPI file, called HTTPEXT.PAS, is appended to the end of this article.

Microsoft's freely distributable Internet Control Pack (ICP) is a set of OCX/ActiveX controls you can drop into Delphi applications. (The Delphi 2.0 update ships with these controls.) They provide immediate support for creating Delphi apps that know how to brows the WEB, as well as make use of FTP, WINSOCK, and other Internet technologies. If your copy of Delphi didn't ship with these controls, then before you can use them you need to add some files to the Delphi LIB directory. The files you need are available at the Borland INDEX.HTML WEB site listed above. I do not discuss the ICP controls in this paper, but anyone interested in the technology discussed here should definitely make sure they have a copy of these controls.

You can download my Pascal utility files called STRBOX.PAS and MATHBOX.PAS from my web site:

http://users.aol.com/charliecal

In general, it is worth checking this web site for possible updates on information discussed in this paper.

I am assuming that readers of this paper are familiar with Delphi and Object Pascal, and that they have a basic understanding of the Internet, HTML, WEB Browsers, and WEB servers.

ISAPI

ISAPI is a very easy to use, but extremely powerful technology that allows you to extend the power of the Internet Information Server. This tool ships with Windows NT 4.0 Server and allows you to set up WEB, FTP and GOPHER sites on your servers. It is also compatible with Windows NT 3.51 Server.

In the past, the best way to extend a WEB server was to create CGI applications. These were powerful tools, but they were limited by their executable format. When you sent in a CGI based request from a browser to a server the CGI application in question usually had to be loaded into memory, which took a long time. Also, the CGI technology could be a bit awkward to use under some circumstances.

ISAPI is a method of writing DLLs that replace CGI applications. You can also write filters with ISAPI, but I will not discuss that technology in this paper. ISAPI has the advantage of being easier to use than CGI, plus it can be much faster and make much better use of system resources. In particular, the following points help explain why ISAPI DLLS are better than CGI applications:

As mentioned above, you can write filters with ISAPI. Here, according to the Microsoft documentation, are some of the things you can do with ISAP filters:

In this paper, however, I will concentrate on writing DLLs that return data sets, or that simply communicate with the user who is running a browser.

ISAPI BASICS

The file HTTPEXT.PAS contains the key declarations used with ISAPI. This file should ship with versions of Delphi dated June, 1996 or later. It is also available on the Borland WEB site and is appended to the end of the ISAPI section of this document. Because this is an NT based technology, you must be using Delphi 2.0 or higher to access this technology. You can't use it from a 16 bit compiler.

HTTPEXT.PAS contains the interface to the ISAPI technology that has been created by Microsoft. At the time of this writing Delphi has no custom interface for ISAPI, and I will describe only how to use Microsoft's existing technology. However, ISAPI is extremely easy to use, and the edition of a custom Delphi object is not necessary for most users.

There are three functions that can serve as entry points to ISAPI DLLs. The first two listed below are mandetory, while the third is optional:

When you are creating an ISAPI DLL, you must export the first two of the three functions listed above. Implementing these two functions is the key to all ISAPI programming.

These three routines all contain the word Extension. This term is used because ISAPI DLLs extend the Internet Information Server. (Remember, the Internet Information Server is Microsoft's WEB server. If you want to turn an NT server into a WEB server, then this is the tool you use. It ships with NT 4.0, and is installed automatically during the setup of that operating system.)

ISAPI provides a standard which other server manufactures could follow. For instance, it would be nice to simplify access to Netscape's complex NSAPI interface by encapsulating it inside the relative simplicity and elegance of ISAPI.

Here are the declarations for the two mandatory routines:

function GetExtensionVersion(var Ver: THSE_VERSION_INFO): 
  BOOL; stdcall;
function HttpExtensionProc(var ECB: TExtensionControlBlock): 
  DWORD; stdcall;

You can always just cut and past the GetExtensionVersion code into your DLLs. You would need to change the function only slightly when new releases of ISAPI are made public:

function GetExtensionVersion(var Ver: THSE_VERSION_INFO): 
  BOOL; stdcall;
begin
  Ver.dwExtensionVersion := $00010000;  // 1.0 support
  Ver.lpszExtensionDesc := 'Delphi 2.0 ISAPI DLL'; // Description
  Result := True;
end;

The parameter passed to this function is declared in HTTPEXT.PAS as follows:

PHSE_VERSION_INFO = ^THSE_VERSION_INFO;
THSE_VERSION_INFO = packed record
  dwExtensionVersion: DWORD;
  lpszExtensionDesc: array[0..HseMaxExtDLLNameLen-1] of Char;
end;

HseMaxExtDLLNameLen is declared to have a value of 256. The two fields of the record are self explanatory, with the first containing the ISAPI version number and the second holding a user defined string describing the purpose of the DLL.

At the same time that you write the GetExtensionVersion routine, you would want to add an exports section to the DPR file for your DLL. You might as well export both mandatory functions at the same time when you write this part of the DLL:

exports
  GetExtensionVersion,
  HttpExtensionProc;

That's all you need to do to set up the first of the two mandatory functions in an ISAPI DLL. The next step, using HttpExtensionProc, is a bit more complex, so I will treat it in its own section.

WORKING WITH HTTPEXTENSIONPROC

The HttpExtensionProc routine is the entry point for the DLL. It serves the same purpose that the main() routine does in a C program, or that the main begin..end pair does in a Delphi program.

Here is a very simple example of a GetExtensionVersion routine:

function HttpExtensionProc(var ECB: TExtensionControlBlock):
   DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := '<HTML><TITLE>Test server result</TITLE>' +
            '<H1>Test server results</H1>' +
            '<BODY>Hello from ISAPI<BR></BODY>' +
            '</HTML>';

  ResStr := Format(
    'HTTP/1.0 200 OK'#13#10+
    'Content-Type: text/html'#13#10+
    'Content-Length: %d'#13#10+
    'Content:'#13#10#13#10'%s', [Length(ResStr), ResStr]);

  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;

If you queried this DLL from a browser you would get a page back saying:

Test Server Results

Hello from ISAPI

Most of the body of the function is taken up with simple HTML code that provides basic information. You also need to fill out a few fields of the TExtensionControlBlock, shown below.

Notice that there is a function pointer in the record called WriteClient. You can call this function to send information back to the browser. When calling this function, you use the value in the ConnID field of the TExtensionControl block record shown below. ConnID is filled out for you automatically when the HttpExtensionProc function is called.

Before looking at the TExtensionControlBlock function, let me show you a complete ISAPI DLL that uses the HttpExtensionProc function shown above:


library Isapi1;

library Isapi1;

uses
  Windows, SysUtils, HTTPExt;

function GetExtensionVersion( var Ver: THSE_VERSION_INFO ): BOOL; stdcall;
begin
  Ver.dwExtensionVersion := $00010000;  // We're expecting version 1.0 support
  Ver.lpszExtensionDesc := 'Written in Delphi 2.0';
  Result := True;
end;

function HttpExtensionProc( var ECB: TEXTENSION_CONTROL_BLOCK ): DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := '
' +
            '

Test server results

' +
            '
Isapi says hello to DevRel
';
  ResStr := Format(
    'HTTP/1.0 200 OK'#13#10+
    'Content-Type: text/html'#13#10+
    'Content-Length: %d'#13#10+
    'Content:'#13#10#13#10'%s', [Length(ResStr), ResStr]);
  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;

exports
  GetExtensionVersion,
  HttpExtensionProc;

begin
end.

To use this DLL, you should copy it into a subdirectory of the scripts directory beneath your inetsrv directory. On my NT 4.0 machine, that looks like this:

c:\winnt\system32\inetsrv\scripts\mystuff\isapi1.dll

In this case, I have created the directory called MYSTUFF, and it is used solely for storing ISAPI DLLs that I have created. You milage, may, of course, differ on your machine, depending on where you put the inetsrv directory and various other factors.

To call this DLL, you should add the following hyperlink to one of your HTML pages:

<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>

When the user clicks on this hyperlink, then the ISAPI1 DLL will be called and the string "Hello from ISAPI" will appear in the user's browser. If you did not put the ISAPI1.DLL in the MYSTUFF directory then you should change the above HTML code to reflect that fact. Notice that the path you assign is relative to the INETSRV directory, and does not, and should not, contain the entire path to your DLL.

Here is a complete HTML script that will call the ISAPI.DLL:

<HTML>
<HEAD>
<TITLE>CharlieC Home Page</TITLE>
</HEAD>
<BODY>
<H1>My Home Page </H1>
<P>
This is the home page for my home computer.
<P>
<A HREF="/scripts/mystuff/isapi1.dll" >ISAPI One</A><BR>
</BODY>
</HTML>

Note that if you copy the ISAPI1.DLL into the MYSTUFF directory multiple times you will need to shut down the WWW portion of the Internet Server before each copy. The rule is that you can copy the DLL the fist time for free, but after you have used it, it belongs to the server, and you need to shut the WWW services on the server down before you can copy an updated version of the file over the first copy. You can use the Internet Service Manager application to shut down the WWW services. This application should be in the Microsoft Internet Server group created in the Explorer/Program Manager at the time of the install of the Internet Information Server.

WORKING WITH TEXTENSIONCONTROLBLOCK

By this point in the paper you should have been able to create you first ISAPI DLL and call it from a WEB browser on a second machine. The rest of the ISAPI section of this paper explores ISAPI in more depth.

Here is the fairly complex record that is passed as the sole parameter to HttpExtensionProc:

PExtensionControlBlock = ^TExtensionControlBlock;
TExtensionControlBlock = packed record
  cbSize: DWORD;           // = sizeof(TExtensionControlBlock)
  dwVersion: DWORD;        // version info of this spec
  ConnID: HCONN;           // Context Do not modify!
  dwHttpStatusCode: DWORD; // HTTP Status code
  // null terminated log info specific to this Extension DLL
  lpszLogData: array [0..HSE_LOG_BUFFER_LEN-1] of Char;
  lpszMethod: PChar;            // REQUEST_METHOD
  lpszQueryString: PChar;       // QUERY_STRING
  lpszPathInfo: PChar;          // PATH_INFO
  lpszPathTranslated: PChar;    // PATH_TRANSLATED
  cbTotalBytes: DWORD;          // Total bytes from client
  cbAvailable: DWORD;           // Available number of bytes
  lpbData: Pointer;             // pointer to cbAvailable bytes
  lpszContentType: PChar;       // Content type of client data

  GetServerVariable: TGetServerVariableProc;
  WriteClient: TWriteClientProc;
  ReadClient: TReadClientProc;
  ServerSupportFunction: TServerSupportFunctionProc;
end;

Notice that this record contains the ConnID field referenced above, and passed as the first parameter to WriteClient.

The first parameter of this record is used for version control. It should be set to the size of the TExtensionControlBlock. If Microsoft changes this structure, then they can tell which version of the structure they are dealing with by checking the size of the record as recorded in this field. You should never change any of the first three fields of this record, they are filled out ahead of time by ISAPI, and can only be referenced, but not changed, by your program.

The most important field of this record is probably the lpszQueryString, which contains information about the query passed in from the server. For instance, suppose you have created a DLL called ISAPI1.DLL. To call this DLL, you would create an HREF in one of your browser pages that looked like this:

<A HREF="/scripts/mystuff/test1.dll">Test One</A> 

If you wanted to query the DLL, you would edit the above line so it looked like this:

<A HREF="/scripts/mystuff/test1.dll?MyQuery">Test One</A> 

Given the second of the two HTML fragments listed above, your DLL would get called with the string "MyQuery" in the lpszQueryString parameter. Notice in particular the use of the question mark, followed by the query string itself.

You could, of course, change the query string at will. For instance, you could write:

<A HREF="/scripts/mystuff/test1.dll?ServerName">Test One</A> 

To this query, the DLL might reply with the name of the server. There are no limits as to what you can pass in this parameter. It could be anything you want, and it is up to you to parse the information from inside the DLL as you like.

When you return information from the server back to the browser, you use the WriteClient function pointer which is part of this record. You don't have to do anything to initialize this pointer; it is passed to you gratis by the Internet Information Server.

Writers of CGI apps will notice that the syntax for sending query strings is familiar. Indeed, ISAPI follows many of the conventions of CGI, and most of the fields in the TExtensionControlBlock are simply borrowed directly from CGI technology.

Another key field in the TExtensionControlBlock is the lpbData field, which contains any additional information sent to you by the browser. For instance, if you have an HTML form with a number of fields in it, the information from these fields will be sent in the pointer called lpbData. The next section of this paper, called 'Getting Information from a Submit Button', focuses on how to handle this situation.

So far I have zeroed in on four key fields of the TExtensionControlBlock:

  1. WriteClient: A pointer to a function which allows you to send formated HTML data back to the browser. This function uses the ConnID field of TExtensionControlBlock.
  2. lpszQueryString: The query passed to you from the Browser.
  3. lpbData: Any additional data being passed to you from the browser. This is usually the contents of any fields on an HTML form. I discuss this field further below in the section on the Submit Button.

The best way to get a feeling for how the rest of the fields of TExtensionControlBlock work is simply to mirror them back to yourself in a browser. In other words, you would want to create an HTML page that allows the user to call a custom ISAPI DLL. The purpose of this ISAPI DLL is simply to snag the contents of each of the fields of the TExtensionControlBlock, format them in HTML, and send them back to the browser. This will turn your browser into a somewhat funky debugger that shows each of the fields in the TExtensionControlBlock.

Here is a program, written by Borland employee Danny Thorpe, that performs this task:

library test1;

uses
  Windows,
  SysUtils,
  HTTPExt;

function GetExtensionVersion( var Ver: THSE_VERSION_INFO ): 
  BOOL; stdcall;
begin
  Ver.dwExtensionVersion := $00010000;  // 1.0 support
  Ver.lpszExtensionDesc := 'A test DLL written in Delphi 2.0';
  Result := True;
end;


function HttpExtensionProc( var ECB: TEXTENSION_CONTROL_BLOCK ): 
  DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
  Buf: array [0..1024] of Char;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := Format(
    '<HTML><TITLE>Test server result</TITLE>' +
    '<H1>Test server results</H1>' +
    'Size = %d<BR>'+
    'Version = %.8x<BR>'+
    'ConnID = %.8x<BR>'+
    'Method = %s<BR>' +
    'Query = %s<BR>' +
    'PathInfo = %s<BR>'+
    'PathTranslated = %s<BR>'+
    'TotalBytes = %d<BR>'+
    'AvailableBytes = %d<BR>'+
    'ContentType = %s<BR><BR>'+
    '<H1>Some Server Variables</H1>',
    [ECB.cbSize, ECB.dwVersion, ECB.ConnID, 
     ECB.lpszMethod, ECB.lpszQueryString,
     ECB.lpszPathInfo, ECB.lpszPathTranslated, 
      ECB.cbTotalBytes, ECB.cbAvailable,
      ECB.lpszContentType]);
  with ECB do
  begin
    StrLen := Sizeof(Buf);
    GetServerVariable(ConnID, 'REMOTE_ADDR', @Buf, StrLen);
    ResStr := ResStr + 'REMOTE_ADDR = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'REMOTE_HOST', @Buf, StrLen);
    ResStr := ResStr + 'Remote_Host = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'REMOTE_USER', @Buf, StrLen);
    ResStr := ResStr + 'Remote_User = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'SERVER_NAME', @Buf, StrLen);
    ResStr := ResStr + 'SERVER_NAME = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'SERVER_PORT', @Buf, StrLen);
    ResStr := ResStr + 'SERVER_PORT = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'SERVER_PROTOCOL', @Buf, StrLen);
    ResStr := ResStr + 'SERVER_PROTOCOL = '+Buf+'<BR>';
    StrLen := SizeOf(Buf);
    GetServerVariable(ConnID, 'SERVER_SOFTWARE', @Buf, StrLen);
    ResStr := Format('%sSERVER_SOFTWARE = %s<BR>'+
      'ThreadID = %.8x<BR>',[ResStr, Buf, GetCurrentThreadID]);
  end;
  ResStr := ResStr + '</HTML>';
  ResStr := Format(
    'HTTP/1.0 200 OK'#13#10+
    'Content-Type: text/html'#13#10+
    'Content-Length: %d'#13#10+
    'Content:'#13#10#13#10'%s', [Length(ResStr), ResStr]);
  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;


exports
  GetExtensionVersion,
  HttpExtensionProc;

begin
end.

To call this DLL, you should create an HTML script that contains the following line:

<A HREF="/scripts/mystuff/test1.dll">Test One</A> <BR>

GETTING INFORMATION FROM A SUBMIT BUTTON

Often you get information sent to you from an HTML form that has a Submit button on it. As long as this information is shorter than 49 KB, you can assume that it will be available in the lpbData field of TExetensionControlBlock. Here is how you would typically read the information from the pointer passed in this field:

var 
   S: string;
begin
  à
  S := PChar(ECB.lpbData);
  à
end;

If the information passed in this field is larger than 48 KB, then you will need to call ReadClient to get the rest of the information.

If you want to see exactly what information is available in the lpbData field, you can use the following two functions to echo the data back your WEB browser:

function SetUpResString: string;
begin
  Result := '<HTML>' +
            '<TITLE>Test server result</TITLE>' +
            '<H1>Test server results</H1>' +
            '<BODY>lpbData = %s </BODY>' +
            '</HTML>';
end;

function HttpExtensionProc(var ECB: TExtensionControlBlock):
   DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
  S, S1: string;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := SetUpResString;
  S := PChar(ECB.lpbData);
  ResStr := Format(ResStr, [S]);
  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;

Assume that you have an HTML form with the following code in it:

<FORM ACTION="/scripts/mystuff/isapi2.dll" METHOD="POST" 
  ENCTYPE="application/x-www-form-urlencoded">

<P>
Enter Number to Square: <INPUT NAME="GetSquare" VALUE="" 
  MAXLENGTH="25" SIZE=25>
<P>
 <INPUT TYPE=SUBMIT VALUE="Submit" NAME="GetSquare">
</FORM>

This code will produce a form that contains a text area where you can enter a number, and a button called submit. The name of the button is "GetSquare". Given this form, then you can expect the two routines shown above to return the following string, assuming the user enters the number 23 in the text area on the form:

lpbData = GetSquare=23&GetSquare=Submit

To understand what is happening here, note the BODY of the HTML statement composed on the server as reflected in the following excerpt from SetUpResString function shown above:

'<BODY>lpbData = %s </BODY>' +

If you study the code in the HttpExtensionProc function shown above, you will see that it uses the Format routine to substitute the value of ECB.lpbData for the %s variable in the piece of code shown immediately before this sentence. (If you don't understand how Format works, see the Delphi docs.)

Given the form shown above, the value of lpbData sent to the ISAPI DLL when the user presses the submit button is:

GetSquare=23&GetSquare=Submit

For the sake of clarity, let me repeat that the two routines shown above echo this information back to the browser in the following string, which you already saw quoted above:

lpbData = GetSquare=23&GetSquare=Submit

The best way to see this process in action is to run the ISAPI2 program listed below. ISAPI2 is the same as ISAPI1, but it has the new HttpExtensionProc function shown above, also the SetUpResString utility function.

library Isapi2;

uses
  Windows, SysUtils, HTTPExt;

function GetExtensionVersion( var Ver: THSE_VERSION_INFO ): 
  BOOL; stdcall;
begin
  Ver.dwExtensionVersion := $00010000;  // 1.0 support
  Ver.lpszExtensionDesc := 'DLL written in Delphi 2.0';
  Result := True;
end;

function SetUpResString: string;
begin
  Result := '<HTML>' +
            '<TITLE>Test server result</TITLE>' +
            '<H1>Test server results</H1>' +
            '<BODY>lpbData = %s </BODY>' +
            '</HTML>';
end;

function HttpExtensionProc( var ECB: TEXTENSION_CONTROL_BLOCK ): 
  DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
  S, S1: string;
  Len: Integer;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := SetUpResString;
  S := PChar(ECB.lpbData);
  ResStr := Format(ResStr, [S]);
  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;


exports
  GetExtensionVersion,
  HttpExtensionProc;

begin
end.

Once you get the information from the form in the lpbData parameter, you can then parse it and return information to the user. For instance, you could extract the number 23 from the above example and then square it and return it to the user. Doing this would in effect allow you to get information from the user, namely a number, then perform a mathematical action on the number, and return the result to the user. This means you are creating dynamic, interactive WEB pages on the fly, which is the current Sin Qua Non of Internet programming!

Here is the complete code for a program that will square a number submitted to it over the net via a browser:

library Isapi3;

{  This code shows how to take input from the user via a browser,
   parse that information, and then return an answer to the user.
   In particular, the user submits a number, this code squares it,
   and then sends the result back to user.

   Here is the form from the browser that submits the information 
   for parsing:

  <FORM ACTION="/scripts/mystuff/isapi2.dll" METHOD="POST"
  ENCTYPE="application/x-www-form-urlencoded">

  <P>
  Enter Number to Square: <INPUT NAME="GetSquare" VALUE=""
    MAXLENGTH="25" SIZE=25>
  <P>
  <INPUT TYPE=SUBMIT VALUE="Submit" NAME="GetSquare">
  </FORM>

 }

uses
  Windows, SysUtils, HTTPExt,
  StrBox;

function GetExtensionVersion( var Ver: THSE_VERSION_INFO ):
  BOOL; stdcall;
begin
  Ver.dwExtensionVersion := $00010000;  //  version 1.0 support
  Ver.lpszExtensionDesc := 'ISAPI3.DLL';
  Result := True;
end;

// Parse lpbData and retrieve the number the user passed to us.
function ParseData(S: string): Integer;
begin
  S := StripLastToken(S, '&');
  S := StripFirstToken(S, '=');
  Result := StrToInt(S);
end;

function SetUpResString: string;
begin
  Result := '<HTML>' +
            '<TITLE>Test server result</TITLE>' +
            '<H1>Test server results</H1>' +
            '<BODY>Answer = %d </BODY>' +
            '</HTML>';
end;

function HttpExtensionProc( var ECB: TEXTENSION_CONTROL_BLOCK ):
  DWORD; stdcall;
var
  ResStr: string;
  StrLen: Integer;
  S, S1: string;
  Num: Integer;
begin
  ECB.lpszLogData := 'Delphi DLL Log';
  ECB.dwHTTPStatusCode := 200;
  ResStr := SetUpResString;
  S := PChar(ECB.lpbData);
  Num := ParseData(S);
  Num := Sqr(Num);
  ResStr := Format(ResStr, [Num]);
  StrLen := Length(ResStr);
  ECB.WriteClient(ECB.ConnID, Pointer(ResStr), StrLen, 0);
  Result := HSE_STATUS_SUCCESS;
end;


exports
  GetExtensionVersion,
  HttpExtensionProc;

begin
end.

This code might receive the following string from the user who presses the submit button, asking that a number be squared:

GetSquare=5&GetSquare=Submit

Given this input, the code above would return the following string to the user across the internet:

Answer = 25

In short, the user enters the number 5, and you will return to him the number 25. If the user submits the number 10, then you can return the number 100. This all sounds very trivial, but the key issue here is that this activity is taking place on the internet!

The function that parses the data sent by the user looks like this:

// Parse lpbData and retrieve the number the user passed to us.
function ParseData(S: string): Integer;
begin
  S := StripLastToken(S, '&');
  S := StripFirstToken(S, '=');
  Result := StrToInt(S);
end;

The StripLastToken and StripFirstToken routines are in the StrBox united referenced at the beginning of this paper, and included on my web site.

That's most of what I want to say about ISAPI in this paper. This information should be enough to get you up and running and having some fun with this great technology. Below I briefly touch on some additional information on the GetServerVariable and ReadClient routines, that I have experimented with in only the most limited degree. I have also appended HTTPEXT.PAS to this paper since you can't get anywhere without that key document.

THE GETSERVERVARIABLE AND READCLIENT ROUTINSES

You can use GetServerVariable to retrieve information from a server just as you would request information inside a CGI application. Here is an example of calling the routine:

  Len := HseMaxExtDllNameLen;
  SetLength(S1, Len);
  Dec(Len);
  ECB.GetServerVariable(ECB.ConnID,
                        'CONTENT_LENGTH',
                        PChar(S1),
                        Len);

The code first sets the length of the buffer that will hold the information retrieved from the server. It then calls the server and asks for information. For instance, in this case, it asks for the 'Content Length' of the information sent by the server.

The Microsoft documentation informs us that you can pass the following strings in the second parameter of GetServerVariable:

AUTH_TYPE This contains the type of authentication used. For example, if basic authentication is used, the string will be "basic." For NT challenge-response, it will be "NTLM." Other authentication schemes will have other strings. Since new authentication types can be added to the Internet Server, it is not possible to list all the stsring possibilities. If the string is empty, then no authentication is used.
CONTENT_LENGTH The number of bytes which the script can expect to receive from the client.
CONTENT_TYPE The content type of the information supplied in the body of a POST request.
PATH_INFO Additional path information, as given by the client. This consists of the trailing part of the URL after the script name, but before the query string, if any.
PATH_TRANSLATED This is the value of PATH_INFO, but with any virtual path name expanded into a directory specification.
QUERY_STRING The information which follows the '"?" in the URL that referenced this script.
REMOTE_ADDR The IP address of the client or agent of the client (for example, gateway or firewall) that sent the request.
REMOTE_HOST The hostname of the client or agent of the client (for example, gateway or firewall) that sent the request.
REMOTE_USER This contains the username supplied by the client and authenticated by the server. This comes back as an empty string when the user is anonymous (but authenticated).
UNMAPPED_REMOTE_USER This is the username before any ISAPI ApplicationsPI Filter mapped the user making the request to an NT user account (which appears as REMOTE_USER).
REQUEST_METHOD The HTTP request method.
SCRIPT_NAME The name of the script program being executed.
SERVER_NAME The server's hostname, or IP address, as it should appear in self-referencing URLs.
SERVER_PORT The TCP/IP port on which the request was received.
SERVER_PORT_SECURE A string of either zero or 1. If the request is being handled on the secure port, then this will be 1. Otherwise, it will be zero.
SERVER_PROTOCOL The name and version of the information retrieval protocol relating to this request. This is normally HTTP/1.0.
SERVER_SOFTWARE The name and version of the Web server under which the ISAPI ApplicationsPI DLL program is running.
ALL_HTTP All HTTP headers that were not already parsed into one of the previous variables. These variables are of the form HTTP_<header field name>. The headers consist of a null-terminated string with the individual headers separated by line feeds.
HTTP_ACCEPT Special-case HTTP header. Values of the Accept: fields are concatenated and separated by a comma (","). For example, if the following lines are part of the HTTP header: accept: */*; q=0.1
URL (new for vwesion 2.0) Gives the base portion of the URL.

Note that many of the pieces of information listed above are automatically passed in the TExtensionControlBlock record. You therefore usually do not need to call GetServerVariable, but you can if you need to, particularly if you want to retrieve information from with ReadClient and need to know how much information to read.

Most of the time, you do not need to call ReadClient. However, if the amount of data being sent by the browser is larger than 48 KB, then you will need to call ReadClient to get the rest of the data.


HTTPEXT.PAS

Here is the freely distributable HTTPEXT.PAS file from the Borland WEB site:

{*******************************************************}
{                                                       }
{       Delphi ISAPI Interface                          }
{       Version 1.0 HTTP Server Extension interface.    }
{                                                       }
{       Copyright (c) 1996 Borland International        }
{                                                       }
{*******************************************************}


unit HTTPExt;

interface

uses Windows;

const
  HSE_VERSION_MAJOR         =   1;      // major version of this spec
  HSE_VERSION_MINOR         =   0;      // minor version of this spec
  HSE_LOG_BUFFER_LEN        =  80;
  HSE_MAX_EXT_DLL_NAME_LEN  = 256;

type
  HCONN = THandle;

// the following are the status codes returned by the Extension DLL

const
  HSE_STATUS_SUCCESS                      = 1;
  HSE_STATUS_SUCCESS_AND_KEEP_CONN        = 2;
  HSE_STATUS_PENDING                      = 3;
  HSE_STATUS_ERROR                        = 4;

// The following are the values to request services with the ServerSupportFunction.
//  Values from 0 to 1000 are reserved for future versions of the interface

  HSE_REQ_BASE                             = 0;
  HSE_REQ_SEND_URL_REDIRECT_RESP           = ( HSE_REQ_BASE + 1 );
  HSE_REQ_SEND_URL                         = ( HSE_REQ_BASE + 2 );
  HSE_REQ_SEND_RESPONSE_HEADER             = (_HSE_REQ_BASE + 3 );
  HSE_REQ_DONE_WITH_SESSION                = ( HSE_REQ_BASE + 4 );
  HSE_REQ_END_RESERVED                     = 1000;

//
//  These are Microsoft specific extensions
//

  HSE_REQ_MAP_URL_TO_PATH                  = (HSE_REQ_END_RESERVED+1);
  HSE_REQ_GET_SSPI_INFO                    = (HSE_REQ_END_RESERVED+2);


//
// passed to GetExtensionVersion
//

type
  PHSE_VERSION_INFO = ^THSE_VERSION_INFO;
  THSE_VERSION_INFO = packed record
    dwExtensionVersion: DWORD;
    lpszExtensionDesc: array [0..HSE_MAX_EXT_DLL_NAME_LEN-1] of Char;
  end;

type
  TGetServerVariableProc = function ( hConn: HCONN;
                                      VariableName: PChar;
                                      Buffer: Pointer;
                                      var Size: DWORD ): BOOL stdcall;

  TWriteClientProc = function ( ConnID: HCONN;
                                Buffer: Pointer;
                                var Bytes: DWORD;
                                dwReserved: DWORD ): BOOL stdcall;

  TReadClientProc  = function ( ConnID: HCONN;
                                Buffer: Pointer;
                                var Size: DWORD ): BOOL stdcall;

  TServerSupportFunctionProc = function ( hConn: HCONN;
                                          HSERRequest: DWORD;
                                          Buffer: Pointer;
                                          var Size: DWORD;
                                          var DataType: DWORD ): BOOL stdcall;

//
// passed to extension procedure on a new request
//
type

  PEXTENSION_CONTROL_BLOCK = ^TEXTENSION_CONTROL_BLOCK;
  TEXTENSION_CONTROL_BLOCK = packed record
    cbSize: DWORD;                    // size of this struct.
    dwVersion: DWORD;                 // version info of this spec
    ConnID: HCONN;                    // Context number not to be modified!
    dwHttpStatusCode: DWORD;          // HTTP Status code
             // null terminated log info specific to this Extension DLL
    lpszLogData: array [0..HSE_LOG_BUFFER_LEN-1] of Char;
    lpszMethod: PChar;                // REQUEST_METHOD
    lpszQueryString: PChar;           // QUERY_STRING
    lpszPathInfo: PChar;              // PATH_INFO
    lpszPathTranslated: PChar;        // PATH_TRANSLATED
    cbTotalBytes: DWORD;              // Total bytes indicated from client
    cbAvailable: DWORD;               // Available number of bytes
    lpbData: Pointer;                 // pointer to cbAvailable bytes
    lpszContentType: PChar;           // Content type of client data

    GetServerVariable: TGetServerVariableProc;
    WriteClient: TWriteClientProc;
    ReadClient: TReadClientProc;
    ServerSupportFunction: TServerSupportFunctionProc;
  end;

//
//  these are the prototypes that must be exported from the extension DLL
//

//  function GetExtensionVersion( var Ver: THSE_VERSION_INFO ): BOOL; stdcall;
//  function HttpExtensionProc( var ECB: TEXTENSION_CONTROL_BLOCK ): DWORD; stdcall;

// the following type declarations is for the server side

// typedef BOOL  (WINAPI * PFN_GETEXTENSIONVERSION)( HSE_VERSION_INFO  *pVer );
// typedef DWORD (WINAPI * PFN_HTTPEXTENSIONPROC )( EXTENSION_CONTROL_BLOCK *pECB );

implementation

end.

FTP USING WININET

You are now in the second section of this paper, which covers WININET. As mentioned above, this section of the paper is totally discreet from the first section.

Let's take a brief look at the code you need to write to use the WININET DLL in an FTP session. This will not be an exhaustive study, but it should help to get you up and running. The first thing you need to know about this technology is that some of the functions in WININET.PAS return a pointer variable declared to be of type HINTERNET:

var
  HINTERNET: Pointer;

This pointer acts as a handle to the various Internet services you employ. After retrieving the handle, you will pass it in as the first parameter to many of the other WININET function you call throughout the life of a single session.

You need to remember to return the handle to the system when you are through using it, usually by calling the WININET function called InternetCloseHandle:

function InternetCloseHandle(hInet: HINTERNET): BOOL; stdcall;

To get a WININET session started, you call InternetOpen:

function InternetOpen(lpszCallerName: PChar; dwAccessType: DWORD; 
  lpszServerName: PChar; nServerPort: INTERNET_PORT; 
  dwFlags: DWORD): HINTERNET; stdcall;

The first parameter is the name of the application opening the session. You can pass in any string you want in this parameter. Microsoft documentation states that "This name is used as the user agent in the HTTP protocol." The remaining parameters can be set to 0 or nil:

var
  MyHandle: HINTERNET;
à
begin
  MyHandle := InternetOpen('MyApp', 0, nil, 0, 0);
end;

If you want more information on this function download WININET.HLP from www.microsoft.com.

After opening the session, the next step is connect to server using InternetConnect:

function InternetConnect(
   hInet: HINTERNET;           // Handle from InternetOpen
   lpszServerName: PChar;      // Server: i.e., www.borland.com
   nServerPort: INTERNET_PORT; // Usually 0
   lpszUsername: PChar;        // usually anonymous
   lpszPassword: PChar;        // usually your email address
   dwService: DWORD;           // FTP, HTTP, or Gopher?
   dwFlags: DWORD;             // Usually 0
   dwContext: DWORD):          // User defined number for callback
HINTERNET; stdcall;

Here are the three possible, self explanatory, and mutually exclusive, flags that can be passed in the dwService parameter:

INTERNET_SERVICE_FTP
INTERNET_SERVICE_GOPHER
INTERNET_SERVICE_HTTP

Here is the option for the dwFlags parameter:

INTERNET_CONNECT_FLAG_PASSIVE

This option is valid only if you passed INTERNET_SERVICE_FTP in the previous parameter. At this time there are no other valid flags for this parameter.

If the session succeeds InternetOpen returns a valid pointer, otherwise it returns nil.

AFTER CONNECTING

After you are connected, you can call the GetCurrentDirectory to retrieve the name of the current directory:

function TMyFtp.GetCurrentDirectory: string;
var
  Len: Integer;
  S: string;
begin
  Len := 0;
  ftpGetCurrentDirectory(FFTPHandle, PChar(S), Len);
  SetLength(S, Len);
  ftpGetCurrentDirectory(FFTPHandle, PChar(S), Len);
  Result := S;
end;

This function is declared as follows:

function FtpGetCurrentDirectory(
  hFtpSession: HINTERNET;           // handle from InternetConnect
  lpszCurrentDirectory: PChar;      // directory returned here
  var lpdwCurrentDirectory: DWORD): // buf size of 2nd parameter
BOOL; stdcall;                      // True on success

If you set the last parameter to zero, then WININET will use this parameter to return the length of directory string. You can then allocate memory for your string, and call the function a second time in order to retrieve the directory name. This process is shown above in the GetCurrentDirectory method. (Notice the call to SetLength. Delphi requires that you allocate memory for the new long strings in situations like this! The issue here is that the string will be assigned a value inside the operating system, not inside your Delphi application. As a result Delphi can't perform its usual surreptitious strings allocations in these circumstances!)

Here's a set of functions that return the currently available files in a particular directory:

function GetFindDataStr(FindData: TWin32FindData): string;
var
  S: string;
  Temp: string;
begin
  case FindData.dwFileAttributes of
    FILE_ATTRIBUTE_ARCHIVE: S := 'A';
//  FILE_ATTRIBUTE_COMPRESSED: S := 'C';
    FILE_ATTRIBUTE_DIRECTORY: S := 'D';
    FILE_ATTRIBUTE_HIDDEN: S := 'H';
    FILE_ATTRIBUTE_NORMAL: S := 'N';
    FILE_ATTRIBUTE_READONLY: S := 'R';
    FILE_ATTRIBUTE_SYSTEM: S := 'S';
    FILE_ATTRIBUTE_TEMPORARY: S := 'T';
  else
    S := IntToStr(FindData.dwFileAttributes);
  end;
  S := S + GetDots(75);
  Move(FindData.CFilename[0], S[6], StrLen(FindData.CFileName));
  Temp := IntToStr(FindData.nFileSizeLow);
  Move(Temp[1], S[25], Length(Temp));
  Result := S;
end;

function TMyFtp.FindFiles: TStringList;
var
  FindData: TWin32FindData;
  FindHandle: HInternet;
begin
   FindHandle := FtpFindFirstFile(FFtphandle, '*.*',
     FindData, 0, 0);
   if FindHandle = nil then begin
     Result := nil;
     Exit;
   end;
   FCurFiles.Clear;
   FCurFiles.Add(GetFindDataStr(FindData));
   while InternetFindnextFile(FindHandle, @FindData) do
     FCurFiles.Add(GetFindDataStr(FindData));
   InternetCloseHandle(Findhandle);
   GetCurrentDirectory;
   Result := FCurFiles;
end;

The key functions to notice here are ftpFindFirstFile, InternetFindNextFile, and InternetCloseHandle. You use these functions in a manner similar to that employed when calling the Delphi functions FindFirst, FindNext, and FindClose. In particular, you use ftpFindFirstFile to get the first file in a directory. You then call InterentFindNextFile repeatedly, until the function returns False. After finishing the session, call InternetCloseHandle to inform the OS that it can deallocate the memory associated with this process.

I'm not going to explain this process further in this newsletter. If you want more information, you might look up FindFirst in the Delphi help. One final note: Unlike the functions mentioned in the previous paragraph, TWin32FindData is not defined in WININET.PAS, but instead can be found in the WIN32 help file that ships with Delphi. It is declared in the WINDOWS.PAS file that ships with Delphi.

RETRIEVING A FILE

You can use the ftpGetFile function from WININET.PAS to retrieve a file via FTP:

function FtpGetFile(
  hFtpSession: HINTERNET;        // Returned by InternetConnect
  lpszRemoteFile: PChar;         // File to get
  lpszNewFile: PChar;            // Where to put it on your PC
  fFailIfExists: BOOL;           // Overwrite existing files?
  dwFlagsAndAttributes: DWORD;   // File attribute-See CreateFile.
  dwFlags: DWORD;                // Binary or ASCII transfer
  dwContext: DWORD):             // Usually zero
BOOL stdcall;                    // True on success

Here is an example of how to use this call:

function TMyFtp.GetFile(FTPFile, NewFile: string): Boolean;
begin
  Result := FtpGetFile(FFTPHandle, PChar(FTPFile), PChar(NewFile),
               False, File_Attribute_Normal,
               Ftp_Transfer_Type_Binary, 0);
end;

To learn about the parameters that can be passed in the dwFlagsAndAttributes parameter, look up CreateFile in the WIN32 help file that ships with Delphi. The dwFlags parameter can be set to either Ftp_Transfer_Type_Binary, or Ftp_Transfer_Type_Ascii.

EXAMPLE CONTROL

The following Delphi control gives you a starting place for building a visual tool for performing Delphi WININET FTP sessions. As is, the control let's you use the Object Inspector to define the RemoteServer, UserID and Password. The code also automatically returns the current directory in a TStringList and allows you to perform file transfers:

unit Ftp1;

{ FTP example using WININET.PAS rather than 
  an ACTIVEX control. Requires WININET.PAS and
  WININET.DLL. WININET.DLL you can get from 
  Microsoft, WININET.PAS is available from
  www.borland.com, or with some versions of
  Delphi 2.0. 
  
  You might Respond to OnNewDir events as follows:

  procedure TForm1.FTP1NewDir(Sender: TObject);
  begin
    ListBox1.Items := MyFtp1.FindFiles; // Get the directory list
  end;   
}

interface

uses
  Windows, Classes, WinINet,
  SysUtils;
  
type
  TMyFtp = class(TComponent)
  private
    FContext: Integer;
    FINet: HInternet;
    FFtpHandle: HInternet;
    FCurFiles: TStringList;
    FServer: string;
    FOnNewDir: TNotifyEvent;
    FCurDir: string;
    FUserID: string;
    FPassword: string;
    function GetCurrentDirectory: string;
    procedure SetUpNewDir;
  protected
    destructor Destroy; override;
  public
    constructor Create(AOwner: TComponent); override;
    function Connect: Boolean;
    function FindFiles: TStringList;
    function ChangeDirExact(S: string): Boolean;
    function ChangeDirCustom(S: string): Boolean;
    function BackOneDir: Boolean;
    function GetFile(FTPFile, NewFile: string): Boolean;
    function SendFile1(FTPFile, NewFile: string): Boolean;
    function SendFile2(FTPFile, NewFile: string): Boolean;
    function CustomToFileName(S: string): string;
  published
    property CurFiles: TStringList read FCurFiles;
    property CurDir: string read FCurDir;
    property UserID: string read FUserID write FUserID;
    property Password: string read FPassword write FPassword;
    property Server: string read FServer write FServer;
    property OnNewDir: TNotifyEvent read FOnNewDir 
                write FOnNewDir;
  end;

procedure Register;

implementation

uses
  Dialogs;

// A few utility functions

function GetFirstToken(S: string; Token: Char): string;
var
  Temp: string;
  Index: INteger;
begin
  Index := Pos(Token, S);
  if Index < 1 then begin
    GetFirstToken := '';
    Exit;
  end;
  Dec(Index);
  SetLength(Temp, Index);
  Move(S[1], Temp[1], Index);
  GetFirstToken := Temp;
end;

function StripFirstToken(S: string; Ch: Char): string;
var
  i, Size: Integer;
begin
  i := Pos(Ch, S);
  if i = 0 then begin
    StripFirstToken := S;
    Exit;
  end;
  Size := (Length(S) - i);
  Move(S[i + 1], S[1], Size);
  SetLength(S, Size);
  StripFirstToken := S;
end;

function ReverseStr(S: string): string;
var
  Len: Integer;
  Temp: String;
  i,j: Integer;
begin
  Len := Length(S);
  SetLength(Temp, Len);
  j := Len;
  for i := 1 to Len do begin
    Temp[i] := S[j];
    dec(j);
  end;
  ReverseStr := Temp;
end;

function StripLastToken(S: string; Token: Char): string;
var
  Temp: string;
  Index: INteger;
begin
  SetLength(Temp, Length(S));
  S := ReverseStr(S);
  Index := Pos(Token, S);
  Inc(Index);
  Move(S[Index], Temp[1], Length(S) - (Index - 1));
  SetLength(Temp, Length(S) - (Index - 1));
  StripLastToken := ReverseStr(Temp);
end;


procedure Register;
begin
  RegisterComponents('Unleash', [TMyFtp]);
end;

constructor TMyFtp.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FCurFiles := TStringList.Create;
  FINet := InternetOpen('WinINet1', 0, nil, 0, 0);
end;

destructor TMyFtp.Destroy;
begin
  if FINet <> nil then
    InternetCloseHandle(FINet);
  if FFtpHandle <> nil then
    InternetCloseHandle(FFtpHandle);
  inherited Destroy;
end;

function TMyFtp.Connect: Boolean;
begin
  FContext := 255;
  FftpHandle := InternetConnect(FINet, PChar(FServer), 0,
   PChar(FUserID), PChar(FPassWord),
   Internet_Service_Ftp, 0, FContext);
  if FFtpHandle = nil then
    Result := False
  else begin
    SetUpNewDir;
    Result := True;
  end;
end;

function TMyFtp.GetCurrentDirectory: string;
var
  Len: Integer;
  S: string;
begin
  Len := 0;
  ftpGetCurrentDirectory(FFTPHandle, PChar(S), Len);
  SetLength(S, Len);
  ftpGetCurrentDirectory(FFTPHandle, PChar(S), Len);
  Result := S;
end;

procedure TMyFtp.SetUpNewDir;
begin
  FCurDir := GetCurrentDirectory;
  if Assigned(FOnNewDir) then
    FOnNewDir(Self);             
end;

function GetDots(NumDots: Integer): string;
var
  S: string;
  i: Integer;
begin
  S := '';
  for i := 1 to NumDots do
    S := S + ' ';
  Result := S;
end;

function GetFindDataStr(FindData: TWin32FindData): string;
var
  S: string;
  Temp: string;
begin
  case FindData.dwFileAttributes of
    FILE_ATTRIBUTE_ARCHIVE: S := 'A';
//    FILE_ATTRIBUTE_COMPRESSED: S := 'C';
    FILE_ATTRIBUTE_DIRECTORY: S := 'D';
    FILE_ATTRIBUTE_HIDDEN: S := 'H';
    FILE_ATTRIBUTE_NORMAL: S := 'N';
    FILE_ATTRIBUTE_READONLY: S := 'R';
    FILE_ATTRIBUTE_SYSTEM: S := 'S';
    FILE_ATTRIBUTE_TEMPORARY: S := 'T';
  else
    S := IntToStr(FindData.dwFileAttributes);
  end;
  S := S + GetDots(75);
  Move(FindData.CFilename[0], S[6], StrLen(FindData.CFileName));
  Temp := IntToStr(FindData.nFileSizeLow);
  Move(Temp[1], S[25], Length(Temp));
  Result := S;
end;

function TMyFtp.FindFiles: TStringList;
var
  FindData: TWin32FindData;
  FindHandle: HInternet;
begin
   FindHandle := FtpFindFirstFile(FFtphandle, '*.*',
     FindData, 0, 0);
   if FindHandle = nil then begin
     Result := nil;
     Exit;
   end;
   FCurFiles.Clear;
   FCurFiles.Add(GetFindDataStr(FindData));
   while InternetFindnextFile(FindHandle, @FindData) do
     FCurFiles.Add(GetFindDataStr(FindData));
   InternetCloseHandle(Findhandle);
   GetCurrentDirectory;
   Result := FCurFiles;
end;

function TMyFtp.CustomToFileName(S: string): string;
const
  PreSize = 6;
var
  Temp: string;
  TempSize: Integer;
begin
  Temp := '';
  TempSize := Length(S) - PreSize; 
  SetLength(Temp, TempSize);
  Move(S[PreSize], Temp[1], TempSize);
  Temp := GetFirstToken(Temp, ' ');
  Result := Temp;
end;

function TMyFtp.BackOneDir: Boolean;
var
  S: string;
begin
  S := FCurDir;
  S := StripLastToken(S, '/');
  if S = '/' then begin
    Result := False;
    Exit;
  end;

  if S <> '' then begin
    ChangeDirExact(S);
    Result := True;
  end else begin
    ChangeDirExact('/');
    Result := True;
  end;

end;

// Changes to specific directory in S
function TMyFtp.ChangeDirExact(S: string): Boolean;
begin
  if S <> '' then
    FtpSetCurrentDirectory(FFTPHandle, PChar(S));
  Result := True;
  FindFiles;
  SetUpNewDir;
end;

// Assumes S has been returned by GetFindDataString;
function TMyFtp.ChangeDirCustom(S: string): Boolean;
begin
  S := CustomToFileName(S);
  if S <> '' then
    FtpSetCurrentDirectory(FFTPHandle, PChar(S));
  Result := True;
  FindFiles;
  SetUpNewDir;
end;

function TMyFtp.GetFile(FTPFile, NewFile: string): Boolean;
begin
  Result := FtpGetFile(FFTPHandle, PChar(FTPFile), PChar(NewFile),
               False, File_Attribute_Normal,
               Ftp_Transfer_Type_Binary, 0);
end;

function TMyFtp.SendFile1(FTPFile, NewFile: string): Boolean;
const
  Size:DWord = 3000;
var
  Transfer: Bool;
  Error: DWord;
  S: string;
begin
  Transfer := FtpPutFile(FFTPHandle, PChar(FTPFile), 
                         PChar(NewFile),
                         Ftp_Transfer_Type_Binary, 0);

  if not Transfer then begin
    Error := GetLastError;
    ShowMessage(Format('Error Number: %d. Hex: %x', 
                       [Error, Error]));
    SetLength(S, Size);
    if not InternetGetLastResponseInfo(Error, PChar(S), Size) then 
    begin
      Error := GetLastError;
      ShowMessage(Format('Error Number: %d. Hex: %x', 
        [Error, Error]));
    end;
    ShowMessage(Format('Error Number: %d. Hex: %x Info: %s', 
                       [Error, Error, S]));
  end else
    ShowMessage('Success');
  Result := Transfer;
end;

function TMyFtp.SendFile2(FTPFile, NewFile: string): Boolean;
var
  FHandle: HInternet;
begin
  FHandle :=  FtpOpenFile(FFTPHandle, 'sam.txt', GENERIC_READ,
                           FTP_TRANSFER_TYPE_BINARY, 0);
  if FHandle <> nil then
  InternetCloseHandle(FHandle)
  else
    ShowMessage('Failed');
  Result := True;
end;

end.