|
CASSINI Spy - Felix John COLIBRI.
|
- abstract : a sniffing utility which captures and display all HTTP packets
between the Cassini Web Server and your Browser
- key words : ASP.NET development, CASSINI, HTTP sniffer
- software used : Windows XP, Delphi 2006, Delphi 2005
- hardware used : Pentium 2.800Mhz, 512 M memory, 250 G hard disc
- scope : Delphi 8, 2005, 2006, Windows C# framework
- level : Delphi / ASP.NET developer
- plan :
1 - Introduction
When debugging ASP.NET applications, it is very helpful to display the content
of the information exchanged between the browser (Internet Explorer) and the
Web Server (CASSINI or IIS).
ASP.NET can log all kind of information, but the content of what is exchanged
between the Client and the Server is not, as far as I know, available.
We first tried to use our own TCP/IP Sniffer. But since
CASSINI works on localhost, the packet do not travel up to the Ethernet
Driver, where the Packet Sniffer would catch them.
So the alternative we found was to build a specific tool that we will present
now.
2 - How it Works
A simple descriptioin of Client / Server socket operation is the following:
- the Server (Cassini in our case) loads, opens a Server Socket which
starts listening to incoming Client for HTTP requests (port 80):
- "a" Client (Internet Explorer in our case) loads, creates a Client Socket
which tries to connect to a port 80 Server (Cassini):
- the Server "accepts" the connection. Since the Server is supposed to
handle many Clients, it cannot take care of the communication with all
Clients at the same time. This is why is spins-off a "Server Client Socket"
for each incoming Client:
- the Server sends the answer back, in one or several packets, and the
Client happily communicates with his "Server Client Socket":
- the communication is closed down when either client sockets closes (or an
error occurs somewhere in the network)
In the case of Cassini / Internet Explorer, the port uses is by default the
HTTP port, 80. But Cassini, in its staring dialog, allows to specify the
listening port. This will allow the developper to still use the standard 80
port to do bona fide HTTP work on his PC (to Google around or whatever)
And similarily, Internet Explorer allows us to specify a connection port in
the address combo box:
Since we can specify both Server and Client ports, we simply place our
sniffing utility in the middle, communicating with Cassini on, say, port 55,
and with Internet Explorer on, say, port 81:
- Cassini and our spy start listening:
- Internet Explorer starts, and tries to connect to port 81:
- our spy spins off a Server Client Socket which receives the Internet
Explorer request:
- our spy creates a Client Socket which will now communicate with Cassini:
- this Client Socket forwards the IE request to Cassini, which spins off a
Server Client Socket :
- the packets then flow between Cassini and Internet Explorer, with our spy
sitting in the middle, capturing the packets and doing whatever it wants
with those packets (saving them to disk, formating them and displaying them
in a window etc.) :
We showed Internet Explorer, our spy and Cassini on 3 different PCs. In
fact, Cassini is by definition a LOCAL Server. It can only handle request
from a browser on the SAME machine. So our figure should represent everything
on the same PC. But the idea is the same.
To implement our spy, we simple build a Delphi application with
- a Server Socket listening to Internet Explorer
- the request from Internet Explorer will spin off a Server Client
Socket, which will:
- forward the request to Cassini
- watch for Cassini's answers, and shovel them back to Internet Explorer
- all the packets will be displayed, in different formats, in tMemos of our
application
3 - Delphi Source Code
3.1 - Socket components
It is possible to use the standard Delphi tServerSocket and tClientSocket
components. You may look at our Delphi Socket
Architecture paper which explain how to use them, with a sample file
transfer project.
We can also use our own Socket components, which were presented in the
Simple Web Server paper, or any other Socket library
(ICS, Indy, Synapse or whatever).
All we have to do is
- override the generic tServer, in order to force him to use our derived
Server Client Socket :
- override the generic Server Client Socket, to let it receive IE packets
and transfer them the the spy Client Socket :
- override the generic tClientSocket to let him mimic IE and communicate
with Cassini :
For this simple utility, we will use our library. The overall UML class diagram
is the following:
and at the bottom of the figure are our three sockets.
3.2 - The Server Client Socket
Here is the definition our our derived Server Client Socket
c_cassini_spy_server_client_socket=
class(c_server_client_socket)
m_on_display_cassini_spy_event: t_po_cassini_spy_display_event;
m_cassini_port: Integer;
m_c_cassini_spy_client_socket: c_cassini_spy_client_socket;
Constructor create_server_client_socket(p_name: String;
p_c_server_socket_ref: c_server_socket;
p_cassini_port: Integer); Override;
procedure handle_can_write_data; Override;
procedure handle_received_data; Override;
procedure handle_remote_client_closed; Override;
procedure handle_received_data_from_cassini(
p_c_cassini_spy_client_socket: c_cassini_spy_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_server_client_socket
|
where m_c_cassini_spy_client_socket is the Client Socket communicating
between the Spy and Cassini
There are two interesting methods:
- the event handler which receives packets from Internet Explorer and
forwards them to Cassini using the Client Socket:
procedure c_cassini_spy_server_client_socket.handle_received_data;
// -- received some data from the browser
var l_end_of_header_position: Integer;
l_read_index: Integer;
begin
// -- fetch the bytes
receive_buffered_data;
with m_c_reception_buffer do
begin
if Assigned(m_on_display_cassini_spy_event)
then
with m_c_reception_buffer do
m_on_display_cassini_spy_event(1,
f_display_buffer(m_read_index, m_write_index));
// -- set the text to send to Cassini
m_c_cassini_spy_client_socket.m_c_cassini_spy_text_to_send_ref:=
m_c_reception_buffer;
m_c_cassini_spy_client_socket.connect_tee('127.0.0.1', m_cassini_port);
end; // with p_c_byte_buffer
end; // handle_received_data
|
- the symmetric handler which receives data from Cassini
procedure c_cassini_spy_server_client_socket.handle_received_data_from_cassini(
p_c_cassini_spy_client_socket: c_cassini_spy_client_socket);
// -- now CASSINI did send the answer
procedure send_back(p_c_reception_buffer: c_byte_buffer);
begin
with p_c_reception_buffer do
begin
// -- the server_client_socket sends back
f_do_send_buffer(@ m_oa_byte_buffer[m_read_index], m_write_index);
// -- move forward
m_read_index:= m_write_index;
end; // with p_c_byte_buffer
end; // send_back
begin // handle_received_data_from_cassini
// -- received an answer from Cassini
// -- forward to IE
with p_c_cassini_spy_client_socket, m_c_reception_buffer do
if m_write_index> 0
then begin
if Assigned(m_on_display_cassini_spy_event)
then m_on_display_cassini_spy_event(2,
f_display_buffer(m_read_index, m_write_index));
send_back(m_c_reception_buffer);
end;
end; // handle_received_data_from_cassini
|
3.3 - The Spy Client Socket
Here is the definition our our derived Server Client Socket
c_cassini_spy_client_socket=
class(c_client_socket)
m_c_cassini_spy_text_to_send_ref: c_byte_buffer;
m_on_cassini_spy_received_data: t_po_cassini_spy_client_socket_event;
Constructor create_cassini_spy_client_socket(p_name: String);
procedure trace_cassini_spy_client(p_text: String);
procedure connect_to_cassini(p_server: String; p_port: Integer);
procedure handle_connected(p_c_client_socket: c_client_socket);
procedure handle_received_data(p_c_client_socket: c_client_socket);
procedure handle_remote_server_client_socket_closed(
p_c_client_socket: c_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_client_socket
|
and
- here is the "connected" handler:
c_cassini_spy_client_socket=
class(c_client_socket)
m_c_cassini_spy_text_to_send_ref: c_byte_buffer;
m_on_cassini_spy_received_data: t_po_cassini_spy_client_socket_event;
Constructor create_cassini_spy_client_socket(p_name: String);
procedure trace_cassini_spy_client(p_text: String);
procedure connect_to_cassini(p_server: String; p_port: Integer);
procedure handle_connected(p_c_client_socket: c_client_socket);
procedure handle_received_data(p_c_client_socket: c_client_socket);
procedure handle_remote_server_client_socket_closed(
p_c_client_socket: c_client_socket);
Destructor Destroy; Override;
end; // c_cassini_spy_client_socket
|
- and the method sending the data back to IE:
procedure c_cassini_spy_client_socket.handle_received_data(
p_c_client_socket: c_client_socket);
// -- fd_read notification was received
var l_received_text: String;
begin
// -- notify IE
if Assigned(m_on_cassini_spy_received_data)
then m_on_cassini_spy_received_data(Self);
end; // handle_received_data
|
3.4 - The spy Server Socket
Here is the CLASS definition:
t_po_tcp_cassini_spy_event= Procedure(p_c_cassini_spy: c_cassini_spy)
of object;
c_cassini_spy=
class(c_basic_object)
m_c_server_socket: c_server_socket;
m_cassini_port: Integer;
m_trace_cassini_spy: Boolean;
m_site_path: String;
m_on_accept_tcp_cassini_spy_socket: t_po_tcp_cassini_spy_event;
m_on_display_cassini_spy_event: t_po_cassini_spy_display_event;
Constructor create_cassini_spy(p_name, p_site_path: String;
p_cassini_port: Integer);
procedure start_cassini_spy(p_ie_port: Integer);
procedure handle_accept(p_c_server_socket: c_server_socket;
p_c_server_client_socket: c_server_client_socket);
procedure close_cassini_spy;
Destructor Destroy; Override;
end; // c_cassini_spy
|
where:
- the constructor builds the server Socket
Constructor c_cassini_spy.create_cassini_spy(p_name, p_site_path: String;
p_cassini_port: Integer);
begin
Inherited create_basic_object(p_name);
m_site_path:= p_site_path;
m_cassini_port:= p_cassini_port;
// -- specify which c_server_client_sockt Class should be created
m_c_server_socket:= c_server_socket.create_server_socket('server',
c_cassini_spy_server_client_socket);
m_c_server_socket.m_on_after_accept:= handle_accept;
end; // create_cassini_spy
|
- here the socket starts listening to IE:
procedure c_cassini_spy.start_cassini_spy(p_ie_port: Integer);
begin
with m_c_server_socket do
begin
m_trace_socket:= m_trace_cassini_spy;
wsa_startup;
create_win_socket;
wsa_select;
do_bind_win_socket(p_ie_port);
do_listen_to_client(k_default_listen_queue_size)
end; // with m_c_server_socket
end; // start_cassini_spy
|
- when IE sends data, the Server Socket spins-off a Server Client
Socket:
procedure c_cassini_spy.handle_accept(p_c_server_socket: c_server_socket;
p_c_server_client_socket: c_server_client_socket);
begin
// -- use the same Memo2 display
with (p_c_server_client_socket as c_cassini_spy_server_client_socket) do
begin
m_cassini_port:= m_cassini_port;
m_on_display_cassini_spy_event:= m_on_display_cassini_spy_event;
end;
// -- feed back to the caller
if Assigned(m_on_accept_tcp_cassini_spy_socket)
then m_on_accept_tcp_cassini_spy_socket(Self);
end; // handle_accept
|
3.5 - The main form
The main form simply
- creates the Spy Server Socket:
var g_c_cassini_spy: c_cassini_spy= Nil;
procedure TForm1.go_Click(Sender: TObject);
begin
// -- free any previous spy
g_c_cassini_spy.Free;
g_c_cassini_spy:= c_cassini_spy.create_cassini_spy('cassini_spy',
k_site_path, StrToInt(cassini_port_.Text));
with g_c_cassini_spy do
begin
m_trace_cassini_spy:= True;
start_cassini_spy(StrToInt(ie_port_.Text));
m_on_accept_tcp_cassini_spy_socket:= handle_tcp_cassini_spy_accept;
m_on_display_cassini_spy_event:= handle_cassini_spy_display;
end;
end; // go_pane_lClick
|
- and handle the callback to display the packets in a tMemo
procedure TForm1.handle_cassini_spy_display(p_code: Integer; p_text: String);
// -- 1 from IE, 2 from CASSINI
var l_spaces: String;
procedure do_add(p_text: String);
begin
content_memo_.Lines.Add(l_spaces+ p_text);
raw_content_memo_.Lines.Add(p_text);
end; // do_add
var l_index: Integer;
l_the_line: String;
begin // handle_cassini_spy_display
case p_code of
1 : begin
l_spaces:= '';
do_add('');
do_add(IntToStr(g_request_count)+ ' ================');
Inc(g_request_count);
l_spaces:= ' -> | ';
end;
2 : begin
l_spaces:= '';
do_add('');
do_add('--------------------------------------');
l_spaces:= ' <- | ';
end;
else l_spaces:= '??';
end; // case
l_the_line:= '';
l_index:= 1;
while l_index<= Length(p_text) do
if p_text[l_index]= k_return
then begin
do_add(l_the_line);
l_the_line:= '';
Inc(l_index, 2);
end
else begin
l_the_line:= l_the_line+ p_text[l_index];
Inc(l_index);
end;
// -- flush
if l_the_line<> ''
then do_add(l_the_line);
end; // handle_cassini_spy_display
|
4 - Mini How To
4.1 - The .aspx application
The goal is to display the result of a simple multiplication. We build an .aspx
page, with a simple calculator:
- 2 TextBoxes where the user enters 2 values, a "calculate" Buttons which
submits the input values, and a result TextBox.
- the Button OnClick naturally computes the product
We explained elsewhere how to prepare the .aspx page. Nevertheless, without all
the screen snapshots, here is how to proceed:
|
start Delphi, select "New | ASP.NET Web application - Delphi for .NET"
|
|
using "File | Save as" (or the project manager) rename the page
"a_01_calculator"
|
|
Delphi asks where to put the files and what the project name is
|
|
enter the names. for instance:
and click "Ok"
|
|
drop the 3 TextBox from the "Web Controls" tab of the Palette on the
Form
Drop a Button, and create its Click handler. Compute the result of the
multiplication in the handler. For instance:
procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs);
var l_total: Integer;
begin
l_total:= Convert.ToInt32(Textbox1.Text)* Convert.ToInt32(TextBox2.Text);
TextBox3.Text:= Convert.ToString(l_total);
end;
|
|
Here is the content of the .aspx file (we added labels etc):
<%@ Page Language="c#" Debug="true"
Codebehind="a_01_calculator.pas" AutoEventWireup="false"
Inherits="a_01_calculator.TWebForm1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title></title>
</head>
<body ms_positioning="GridLayout">
<form runat="server">
<asp:TextBox id="TextBox1"
style="Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP: 62px"
runat="server" width="67px"></asp:TextBox>
<asp:TextBox id="TextBox2"
style="Z-INDEX: 2; LEFT: 142px; POSITION: absolute; TOP: 62px"
runat="server" width="75px"></asp:TextBox>
<asp:TextBox id="TextBox3"
style="Z-INDEX: 3; LEFT: 238px; POSITION: absolute; TOP: 62px"
runat="server" width="91px"></asp:TextBox>
<asp:Button id="Button1"
style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute; TOP: 102px"
runat="server" text="multiply"></asp:Button>
<asp:Label id="Label1"
style="Z-INDEX: 5; LEFT: 54px; POSITION: absolute; TOP: 22px"
runat="server">price</asp:Label>
<asp:Label id="Label2"
style="Z-INDEX: 6; LEFT: 126px; POSITION: absolute; TOP: 22px"
runat="server" width="3px">x</asp:Label>
<asp:Label id="Label3"
style="Z-INDEX: 7; LEFT: 150px; POSITION: absolute; TOP: 22px"
runat="server">quantity</asp:Label>
<asp:Label id="Label4"
style="Z-INDEX: 8; LEFT: 222px; POSITION: absolute; TOP: 22px"
runat="server">=</asp:Label>
</form>
</body>
</html>
|
Then
Thats the standard Delphi way of building ASP.NET applications. But what was
exactly exchanged between Internet Explorer and Delphi during those steps ?
Well, enters the CASSINI SPY !
4.2 - Using the CASSINI SPY
After the creation of the .ASPX and .PAS, there are 3 steps:
- start CASSINI
- start the spy
- start IE and request the page
Start CASSINI:
|
manually start up CASSINI by clicking on CassiniWebServer.Exe
|
|
CASSINI presents a parameter dialog
|
|
enter your application path, the port and the virtual path:
and click "Start"
|
|
CASSINI is ready

|
|
click "Start"
|
|
CASSINI presents all the files in our development folder:

|
Start our CASSINI SPY
Start Internet Explorer:
|
start IE and enter the URL of our page (on port 81):
|
|
hit Enter
|
|
the CASSINI SPY displays the IE -> CASSINI packet as well as the answer:

|
By copying the CASSINI SPY result to the ClipBoard, here is the full packet
exchange:
0 ====================================== 405
-> | GET /_01_calculator/a_01_calculator.aspx HTTP/1.1
-> | Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
application/x-shockwave-flash, */*
-> | Accept-Language: fr
-> | Accept-Encoding: gzip, deflate
-> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows
NT 5.1; SV1; .NET CLR 1.1.4322)
-> | Host: localhost:81
-> | Connection: Keep-Alive
-> | Cookie: Name=the_cookie; ASP.NET_SessionId=hlmd5ti4pfwssz452iog15uh
-> |
-> |
-------------------------------------- 1533
<- | HTTP/1.1 200 OK
<- | Server: Cassini/1.0.0.0
<- | Date: Thu, 16 Feb 2006 18:02:04 GMT
<- | X-AspNet-Version: 1.1.4322
<- | Cache-Control: private
<- | Content-Type: text/html; charset=utf-8
<- | Content-Length: 1318
<- | Connection: Close
<- |
<- |
<- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<- |
<- | <html>
<- | <head>
<- | <title></title>
<- | </head>
<- | <body ms_positioning="GridLayout">
<- | <form name="_ctl0" method="post" action="a_01_calculator.aspx"
id="_ctl0">
<- | <input type="hidden" name="__VIEWSTATE"
value="dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L" />
<- |
<- | <input name="TextBox1" type="text" id="TextBox1"
style="width:67px;Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP:
62px" />
<- | <input name="TextBox2" type="text" id="TextBox2"
style="width:75px;Z-INDEX: 2; LEFT: 142px; POSITION: absolute;
TOP: 62px" />
<- | <input name="TextBox3" type="text" id="TextBox3"
style="width:91px;Z-INDEX: 3; LEFT: 238px; POSITION: absolute;
TOP: 62px" />
<- | <input type="submit" name="Button1" value="multiply"
id="Button1"
style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute; TOP: 102px" />
<- | <span id="Label1" style="Z-INDEX: 5; LEFT: 54px; POSITION:
absolute; TOP: 22px">
price</span>
<- | <span id="Label2" style="width:3px;Z-INDEX: 6; LEFT: 126px;
POSITION: absolute; TOP: 22px">
x</span>
<- | <span id="Label3" style="Z-INDEX: 7; LEFT: 150px; POSITION:
absolute; TOP: 22px">
quantity</span>
<- | <span id="Label4" style="Z-INDEX: 8; LEFT: 222px; POSITION:
absolute; TOP: 22px">
=</span>
<- | </form>
<- | </body>
<- | </html>
<- |
|
And here is the result of sending over 15 x 3 and clicking "multiply" :
1 ====================================== 677
-> | POST /_01_calculator/a_01_calculator.aspx HTTP/1.1
-> | Accept: image/gif, image/x-xbitmap, image/jpeg,
image/pjpeg, application/x-shockwave-flash, */*
-> | Referer: http://localhost:81/_01_calculator/a_01_calculator.aspx
-> | Accept-Language: fr
-> | Content-Type: application/x-www-form-urlencoded
-> | Accept-Encoding: gzip, deflate
-> | User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;
SV1; .NET CLR 1.1.4322)
-> | Host: localhost:81
-> | Content-Length: 110
-> | Connection: Keep-Alive
-> | Cache-Control: no-cache
-> | Cookie: Name=the_cookie; ASP.NET_SessionId=hlmd5ti4pfwssz452iog15uh
-> |
-> | __VIEWSTATE=dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L
&TextBox1=15
&TextBox2=3
&TextBox3=
&Button1=multiply
--------------------------------------215
<- | HTTP/1.1 200 OK
<- | Server: Cassini/1.0.0.0
<- | Date: Thu, 16 Feb 2006 18:08:10 GMT
<- | X-AspNet-Version: 1.1.4322
<- | Cache-Control: private
<- | Content-Type: text/html; charset=utf-8
<- | Content-Length: 1350
<- | Connection: Close
<- |
<- |
--------------------------------------1351
<- |
<- | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<- |
<- | <html>
<- | <head>
<- | <title></title>
<- | </head>
<- | <body ms_positioning="GridLayout">
<- | <form name="_ctl0" method="post" action="a_01_calculator.aspx"
id="_ctl0">
<- | <input type="hidden" name="__VIEWSTATE"
value="dDwtMTcyMDIwMDYzNzs7PnErGOMElmmKP3tC3Yl1ObO7Bo1L" />
<- |
<- | <input name="TextBox1" type="text" value="15" id="TextBox1"
style="width:67px;Z-INDEX: 1; LEFT: 54px; POSITION: absolute; TOP:
62px" />
<- | <input name="TextBox2" type="text" value="3" id="TextBox2"
style="width:75px;Z-INDEX: 2; LEFT: 142px; POSITION: absolute;
TOP: 62px" />
<- | <input name="TextBox3" type="text" value="45" id="TextBox3"
style="width:91px;Z-INDEX: 3; LEFT: 238px; POSITION: absolute;
TOP: 62px" />
<- | <input type="submit" name="Button1" value="multiply"
id="Button1" style="Z-INDEX: 4; LEFT: 54px; POSITION: absolute;
TOP: 102px" />
<- | <span id="Label1" style="Z-INDEX: 5; LEFT: 54px;
POSITION: absolute; TOP: 22px">price</span>
<- | <span id="Label2" style="width:3px;Z-INDEX: 6; LEFT: 126px;
POSITION: absolute; TOP: 22px">x</span>
<- | <span id="Label3" style="Z-INDEX: 7; LEFT: 150px;
POSITION: absolute; TOP: 22px">quantity</span>
<- | <span id="Label4" style="Z-INDEX: 8; LEFT: 222px;
POSITION: absolute; TOP: 22px">=</span>
<- | </form>
<- | </body>
<- | </html>
<- |
|
Of particular interest are
- the ViewState value
- the fact that IE only sent back the values of the controls (this is also
the case for CGI)
- the labels are ALSO sent back (I believed only the values of the controls
were sent back)
5 - Improvements
This utility was built in on day. We have the tool, and will use it in the our
forthcoming ASP.NET tutorials.
To change this into an "industrial strength" tool would require the addition of
some automatic invocation: when the developper clicks "Run", we should force
Delphi to launch our tool, and the SPY in turn should start CASSINI.
6 - Download the Sources
Here are the source code files:
The .ZIP file(s) contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any
other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly
mentioned).
- for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path creation etc).
To use the .ZIP:
- create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
The Pascal code uses the Alsacian notation, which prefixes identifier by
program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre,
F_unction, C_lasse etc. This notation is presented in the Alsacian Notation
paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs or had
some problem downloading the file. Resulting corrections will be helpful
for other readers
- we welcome any comment, criticism, enhancement, other sources or reference
suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an
answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow
developpers, add a link to your links page ou mention our articles in
your newsgroup posts when relevant. That's the way we operate: the more
traffic and Google references we get, the more articles we will write.
7 - References
You may look at:
8 - The author
Felix John COLIBRI works at the Pascal
Institute. He programs in Pascal since 1979, and is mainly active in the area
of custom software
development and training, and is a frequent speaker at Borland
Developer Conferences. His web site features
tutorials, technical papers about programming with full downloadable source
code, and the description and calendar of forthcoming Delphi,
Interbase, Asp.Net, Ado.Net and OOP / UML training sessions.
|