2011. május 6., péntek

Write a TCP Redirector using Indy


Problem/Question/Abstract:

Many people ask how to write servers in Indy, this primer goes through how a TCP server is created, and how to redirect all traffic to another remote server. This is the same as port-mapping in firewalls.

Answer:

TCP Protocol Redirection Primer

Contents

1                Introduction
        1.1        Disclaimer
2                Writing the Server
        2.1        Writing the servers OnExecute() method
        2.2        Writing the client connection
        2.3        Forwarding data to/from the server/client
        2.4        Testing the application
3                Where to next ?
4                Another way

1        Introduction

This article explains how to create a TCP protocol redirecter with Delphi 5 or Delphi 6, using the Indy TCP components.

The purpose of this is to show how to accept a real client connection to an IdTCPServer, and then how to further connect to another remote server, and forward all data coming/going to/from the client/server.

In the article well use the resulting application to redirect traffic to the borland.com www site, to port 80. The local port will also be 80, so in effect all traffic coming in on port 80 will be forwarded to borland.com.

1.1        Disclaimer

This article is written by Kim Sandell, September 2002. You can reach me via email at "kim.sandell@nsftele.com" if you have comments or corrections to the material.

This material is free. It may be used in any way the reader sees fit, but at the readers own risk.

2        Writing the Server

In order to write the server, we need to have a new application in delphi, the IdTCPServer component dropped onto the form, and two buttons, one Button1 as a Start button and another Button2 as a Stop button.

The first thing we need to define is the port the IdTCPServer component will listen to. This is done by setting the DefaultPort property of the IcTCPServer1 to 80. (80=www port remember!)

We also need to define the basic functions for the buttons, and create an event handler for incoming client connections.

For the Button1, create an OnClick event handler:

procedure TForm1.Button1Click(Sender: TObject);
begin
  IdTCPServer1.Active := True;
end;

and for the Button2, create an OnClick event handler:

procedure TForm1.Button2Click(Sender: TObject);
begin
  IdTCPServer1.Active := False;
end;

Go to the properties of the Events of the IdTCPServer and create an event handler for the OnExecute event.

procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
begin

end;

2.1        Writing the servers OnExecute() method

The OnExecute() event is the place where the client connects to. The parameter "AThread" that is passed with it, is the actial thread with all the information about the connection.

In this demo we will use only a few of these, but when you get the hang of all of this, you can start experimenting a bit with the AThread object.

First we need to make sure that everything that happends in the handler is thread-safe. This means we can not update any visual properties, nor can we use any global variables/objects.

We also want to make sure that if anything goes wrong we can handle the situation and cleanup after the client.

We will begin by making a "Try Except End" plus a "Try Finally End" statement wrapped inside each other.

procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
begin
  try
    try
    finally
    end;
  except
  end;
end;

Everything that we do in the OnExecute() method must go inside the try except statement, and most of it will go in the try finally statement.

2.2        Writing the client connection

To make a TCP Client connection to another server/machine we need the IdTCPClient component. We have to make a local variable so that we are thread-safe (see above). This component is found in the IdTCPClient pas file, so we need to include that in the Uses clause in the "implementation" section:

{...}
var
  Form1: TForm1;

implementation

uses IdTCPClient; // This line includes the correct component

{$R *.dfm}
{...}

The TCPClient variable well use is called CLI. This has to be declared in the OnExecute() method locally. Also we need to create the component, and make sure it is destroyed once the connection is terminated.

We also want to tell it where to connect to once a client connects to the server, so well set the Host and Port properties of it.

In the finallt statement we want to make sure the real client that connected is also disconnected, so well throw in a Disconnect for the AThread.Connection as well.

procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var
  Cli: TIdTCPClient;
begin
  try
    Cli := nil;
    try
      { Create & Connect to Server }
      Cli := TIdTCPClient.Create(nil);
      Cli.Host := 'www.borland.com';
      Cli.Port := 80;
    finally
      if Assigned(Cli) then
      begin
        Cli.Disconnect;
        Cli.Free;
      end;
      { Disconnect real client }
      AThread.Connection.Disconnect;
    end;
  except
  end;
end;

Now when a client connects to us, we need to establish a connection to the remote server, so we immediately try to connect to that server using the Cli.Connect; method.

{...}
try
  { Create & Connect to Server }
  Cli := TIdTCPClient.Create(nil);
  Cli.Host := 'www.borland.com';
  Cli.Port := 80;
  { Connect to the remote server }
  Cli.Connect;
finally
  {...}

If we get an error (exception) here, the code will jump to the finally statement, where the CLI component is freed, and the connection to the Client is disconnected immediately. In effect the only thing the real client sees is that a connection was made but the connection was immediately lost before any data came through.

2.3        Forwarding data to/from the server/client

Now that we have the real client connected, and we also have a connection to the remote server, we want to start forwarding data to/from the client/server.

This will be done for as long as both parties are connected to us, so well need a loop that keeps checking if data is coming or going.

In the loop well check if the real client has any data to send, and also check if the remote server as any data to send to the client.

We also need to check for disconnection on both ends, since the Indy components do not alwyas notify of disconnections.

Last but not least: We need to make sure the loop does not take 100% CPU time, since it is going pretty fast through the loop. To avoid that we will add a Sleep command at the end of the loop.

The complete OnExecute() handler should now look like this:

procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var
  Cli: TIdTCPClient;
  Len: Cardinal;
  Data: string;
begin
  try
    Cli := nil;
    try
      { Create & Connect to Server }
      Cli := TIdTCPClient.Create(nil);
      Cli.Host := 'www.borland.com';
      Cli.Port := 80;
      { Connect to the remote server }
      Cli.Connect;
      { Read/Write loop }
      repeat
        { Read data from Client }
        { Uncomment the line below, depending on your
          INDY version }
        // <9: If AThread.Connection.CurrentReadBufferSize>0 then
        // >9: If Lenght(AThread.Connection.CurrentReadBuffer)>0 then
        begin
          Len := AThread.Connection.CurrentReadBufferSize;
          Data := AThread.Connection.ReadString(Len);
          { Write it to the Server }
          Cli.Write(Data);
        end;
        { Read data from Server }
        if Cli.CurrentReadBufferSize > 0 then
        begin
          Len := Cli.CurrentReadBufferSize;
          Data := Cli.ReadString(Len);
          { Write it to the Server }
          AThread.Connection.Write(Data);
        end;
        { Check for Disconnects }
        Cli.CheckForDisconnect(False);
        Cli.CheckForGracefulDisconnect(False);
        AThread.Connection.CheckForDisconnect(False);
        AThread.Connection.CheckForGracefulDisconnect(False);
        { Release system slizes }
        SleepEx(1, True);
      until (not AThread.Connection.Connected) or (not Cli.Connected);
    finally
      if Assigned(Cli) then
      begin
        Cli.Disconnect;
        Cli.Free
      end;
      { Disconnect real client }
      AThread.Connection.Disconnect;
    end;
  except
  end;
end;

2.4        Testing the application

Compile and run the application.

Try connecting with your browser to the "http://localhost" address.

You should see the borland.com website !!!

Note: This does not work if you must have a proxy defined in your  browser !!

3        Where to next ?

Now that you have learned the basics of redirecting TCP connections, I think the next logial step would be to expand the application with more features.

A few suggestions for small enhancements:

Max number of connected users at one time
A timeout disconnect if nothing happends (no data transferred).
Recognise protocols, such as HTTP and actually block unwanted url's from beeing accessed. This would almost be a proxy, even thoug a proxy needs to check where the client wants to go first.

Some of the suggestions are propably easy to implement, others harder. The level of expertiese the programmer (you) has really is the only limit here, and if you want to learn something new, then learning by doing is the best way.

4        Another way

Here is another way of accomplishing the same thing:

Drop a TIdMappedPortTCP from the Indy Servers tab.
Set the MappedHost and MappedPort properties to the server you want the traffic to be redirected to;
Set the DefaultPort property to the port you want to listen for new client connections.
Set Active to True.
Compile and run.

Same thing accomplished w/o a single line of code. This component handles all the work for you. However, one can handle some of the events such as connects, disconnects and so on.

Nincsenek megjegyzések:

Megjegyzés küldése