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!
This paper contains three big sections, and a number of smaller sections. The
big break down into major topics is as follows:
In general, the WININET and ISAPI sections of this paper are entirely
independent, and you can read them in which ever order you like.
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 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.
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.
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 := '
' + '
' + '
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.
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:
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>
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.
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.
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.
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 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.
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.
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.