unit IdTestHTTP;

{
http://www.schroepl.net/cgi-bin/http_trace.pl
useful to test exactly what headers a web server is receiving from the http client
allows you to check that the headers that http client writes are actually received
by the server, ie not modified by a proxy/firewall after they leave your pc
}

//todo test chunked Transfer-Encoding
//http://www.faqs.org/rfcs/rfc2616.html
//3.6 Transfer Codings, 19.4.6 Introduction of Transfer-Encoding

//todo standardize http ports used

{$I IdCompilerDefines.inc}

interface

//short term solution to allow trying alternate compression implementations
{.$DEFINE INDY_USE_ABBREVIA}

uses
  {$IFDEF INDY_USE_ABBREVIA}
  IdCompressorAbbrevia,
  {$ENDIF}
  {$IFNDEF DOTNET}
  IdCompressorZLibEx,
  {$ENDIF}
  IdZLibCompressorBase,
  IdObjs,
  IdLogDebug,
  IdCoder,
  IdCoderMime,
  IdSys,
  IdGlobal,
  IdContext,
  IdTCPServer,
  IdThreadSafe,
  IdCustomHTTPServer,
  IdHTTPServer,
  IdTest,
  IdHTTP;

type

  {
  GZip compression tested using apache 2.0
  add following to httpd.conf to enable gzip on given types

  LoadModule deflate_module modules/mod_deflate.so
  AddOutputFilterByType DEFLATE text/html text/plain text/xml
  }
  {
  can test if a compressed stream is valid by renaming to .gz and
  opening with compression utility, eg winrar

  see also IdCompressorAbbrevia for a reimplementation
  }
  //Note that you can NOT do this test at all in DotNET because
  //we currently have no Indy code for it.
  {$IFNDEF DOTNET}
  TIdTestHTTP_GZip = class(TIdTest)
  private
    procedure CallbackGet(AContext:TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
  published
    procedure TestCompress;
    procedure TestGZip;
  end;
  {$ENDIF}

  //http://www.io.com/~maus/HttpKeepAlive.html
  //re keep-alive, see TIdHTTPResponseInfo.WriteHeader. old comment, delete?
  TIdTestHTTP_KeepAlive = class(TIdTest)
  private
    FRequestCount:TIdThreadSafeInteger;
    FServerConnectCount:TIdThreadSafeInteger;
    procedure CallbackConnect(AContext:TIdContext);
    //procedure CallbackExecute(AContext:TIdContext);
    procedure CallbackGet(AContext:TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
  published
    //this currently seems unreliable. sometimes server will complete the
    //request, but client returns empty comment
    procedure TestKeepAlive;
  end;

  //tests that redirection commands are followed properly
  TIdTestHTTP_Redirect = class(TIdTest)
  private
    procedure CallbackGet(AContext:TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
  published
    procedure TestRedirect;
  end;

  //basic subclass that keeps statistics
  TIdHTTPStats = class(TIdHTTP)
  protected
    procedure DoOnConnected;override;
  public
    //record actual tcp connections
    CountConnect:Integer;
  end;

  TIdHTTPServerStats = class(TIdHTTPServer)
  end;

  //this test shows a memory leak due to AResponse.KeepAlive being called in a
  //finally block, and raising an exception.
  //seperate test class as it has private callbacks for the server
  //the actual result of this test (a memory leak) will only properly be detected
  //on application shutdown, when running an appropriate memory checker
  TIdTestHTTP_01 = class(TIdTest)
  private
    procedure CallbackExecute(AContext:TIdContext);
  published
    procedure TestMemoryLeak;
  end;

  TIdTestHTTP_02 = class(TIdTest)
  private
    FServer:TIdHTTPServer;
    FClient:TIdHTTP;
    procedure CallbackGet(AContext:TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
  protected
    procedure SetUp;override;
    procedure TearDown;override;
  published
    procedure TestRedirectedDownload;
  end;

implementation

uses IdBaseComponent;

const
  //base64 encoding of gzipped 'hello world'
  //used to test decompression
  cHelloWorldGz='H4sIAAAAAAAAC8tIzcnJVyjPL8pJAQCFEUoNCwAAAA==';
  //and what it decodes to
  cHelloWorld='hello world';

  //document on the server to ask for
  cDocGZip='gz';
  //content encoding constant
  cEncodingGZip='gzip';

  cRedirectData='data';

procedure TIdTestHTTP_01.CallbackExecute(AContext: TIdContext);
//check that the server disconnecting the client doesn't cause
//issues, eg memory leaks
begin
  AContext.Connection.Disconnect;
end;

procedure TIdTestHTTP_01.TestMemoryLeak;
var
 aClient:TIdHTTP;
 aServer:TIdTCPServer;
begin
 aClient:=TIdHTTP.Create(nil);
 aServer:=TIdTCPServer.Create(nil);
 try
 aServer.DefaultPort:=20202;
 aServer.OnExecute:=Self.CallbackExecute;
 aServer.Active:=True;

 //the circumstances for the memory leak also require a ConnectionTimeout
 //haven't investigated why
 aClient.ConnectTimeout:=2000;
 try
 aClient.Get('http://127.0.0.1:20202');
 except
 //we're expecting this exception, ignore it
 end;

 finally
 Sys.FreeAndNil(aClient);
 Sys.FreeAndNil(aServer);
 end;
end;

procedure TIdTestHTTP_KeepAlive.CallbackConnect(AContext: TIdContext);
begin
  FServerConnectCount.Increment;
end;

procedure TIdTestHTTP_KeepAlive.CallbackGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  FRequestCount.Increment;
  if ARequestInfo.Document='/1' then AResponseInfo.ContentText:='a'
  else if ARequestInfo.Document='/2' then AResponseInfo.ContentText:='b'
  else AResponseInfo.ContentText:='Unknown:'+ARequestInfo.Document;
end;

procedure TIdTestHTTP_KeepAlive.TestKeepAlive;
//in order to test that keepAlive is working, should count the connection
//attempts on client and server. repeat for keepAlive=true/false
//http client seems to be biased towards 1.1 (see TIdCustomHTTP.ConnectToHost),
//giving no keep-alive, but server is 1.0, expecting a keep-alive
//a fast-response, high requestcount keep-alive client could expect
//a 3x time reduction for the session
var
  c:TIdHTTPStats;
  aContent:string;
  aServer:TIdHTTPServer;
const
  cUrl='http://127.0.0.1:22280/';
begin
  FServerConnectCount:=TIdThreadSafeInteger.Create;
  FRequestCount:=TIdThreadSafeInteger.Create;
  aServer:=TIdHTTPServer.Create;
  c:=TIdHTTPStats.Create;
  try
    aServer.OnConnect:=CallbackConnect;
    aServer.OnCommandGet:=CallbackGet;
    //server currently like 1.0, requires explicit keep-alive request
    aServer.KeepAlive:=True;
    aServer.DefaultPort:=22280;
    aServer.Active:=True;

    //seems to be the only place to specify on client
    c.Request.Connection:='keep-alive';

    //ensure content is different+correct for each request
    aContent:=c.Get(cUrl+'1');
    Assert(aContent='a',aContent);

    aContent:=c.Get(cUrl+'2');
    Assert(aContent='b',aContent);

    Assert(c.CountConnect=1);
    Assert(FServerConnectCount.Value=1);
    Assert(FRequestCount.Value=2);
  finally
    Sys.FreeAndNil(c);
    Sys.FreeAndNil(aServer);
    Sys.FreeAndNil(FServerConnectCount);
    Sys.FreeAndNil(FRequestCount);
  end;
end;

procedure TIdHTTPStats.DoOnConnected;
begin
  inherited;
  Inc(CountConnect);
end;

procedure TIdTestHTTP_Redirect.CallbackGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if ARequestInfo.Document='/1' then AResponseInfo.Redirect('/2')
  else if ARequestInfo.Document='/2' then AResponseInfo.ContentText:='b';
end;

procedure TIdTestHTTP_Redirect.TestRedirect;
var
  aClient:TIdHTTP;
  aServer:TIdHTTPServer;
  s:string;
  aCorrectClass:boolean;
  aClassName:string;
begin
  aClient:=TIdHTTP.Create;
  aServer:=TIdHTTPServer.Create;
  try
  aServer.OnCommandGet:=CallbackGet;
  aServer.DefaultPort:=22280;
  aServer.Active:=True;

  //this shouldnt be needed
  aClient.ReadTimeout:=10000;

  //first, try without redirects enabled. should get an exception.
  aClient.HandleRedirects:=False;
  try
  aCorrectClass:=True;
  s:=aClient.Get('http://127.0.0.1:22280/1');
  Assert(False);//should not get here
  except
  //expect to get here
  //check exception class. dont raise exception in except-block.
  on e:Exception do
   begin
   aClassName:=e.ClassName;
   aCorrectClass:=e is EIdHTTPProtocolException;
   end;
  end;
  //also check code (302)
  Assert(aCorrectClass,aClassName);

  //now let indy handle the redirection instructions from the server
  aClient.HandleRedirects:=True;
  s:=aClient.Get('http://127.0.0.1:22280/1');
  Assert(s='b',s);
  Assert(aClient.RedirectCount=1);

  //todo test that RedirectMaximum is followed

  finally
  Sys.FreeAndNil(aClient);
  Sys.FreeAndNil(aServer);
  end;
end;

{
  //use to encode test data so it can be included in source easily
  aEncode:=TIdEncoderMIME.Create;
  aStream:=TIdMemoryStream.Create;
  try
  aStream.LoadFromFile('e:\test.txt');
  s:=aEncode.Encode(aStream);
  assert(s<>'');
  finally
  sys.FreeAndNil(aEncode);
  end;
  Exit;
}

{$IFNDEF DOTNET}
procedure TIdTestHTTP_GZip.CallbackGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo;
  AResponseInfo: TIdHTTPResponseInfo);
//currently content has to be manually compressed
//eg server doesnt auto-compress all "plain/text" content if client allows
begin
  if ARequestInfo.Document='/'+cDocGZip then
    begin
    //todo reply based on the requests ContentEncoding string
    AResponseInfo.ContentEncoding:=cEncodingGZip;
    AResponseInfo.ContentText:=DecodeString(TIdDecoderMIME,cHelloWorldGz);
    end
  else
    begin
    AResponseInfo.ContentText:='error';
    end;
end;

procedure TIdTestHTTP_GZip.TestCompress;
//tests the client correctly decompressed gzip content received from server
//basically the same as TestGZip but with the httpclient+server too
//todo also deflate?
var
  aClass:TIdZLibCompressorBaseClass;
  aCompress:TIdZLibCompressorBase;
  aClient:TIdHTTP;
  aStream:TIdStringStream;
  aServer:TIdHTTPServer;
begin
  {$IFDEF DOTNET}
  aClass:=nil;
  {$ELSE}
    {$IFDEF INDY_USE_ABBREVIA}
    aClass:=TIdCompressorAbbrevia;
    {$ELSE}
    aClass:=TIdCompressorZLibEx;
    {$ENDIF}
  {$ENDIF}

  //todo test that server only returns compressed data if we ask for it

  Assert(aClass<>nil);
  aCompress:=aClass.Create;
  aServer:=TIdHTTPServer.Create;
  aClient:=TIdHTTP.Create;
  aStream:=TIdStringStream.Create('');
  try
    aServer.DefaultPort:=22280;
    aServer.OnCommandGet:=Self.CallbackGet;
    aServer.Active:=True;

    aClient.Compressor:=aCompress;
    //identity is due to existing code in http unit, may be changed/removed in future.
    aClient.Request.AcceptEncoding := 'gzip, deflate, identity';
    aClient.CreateIOHandler;
    //aClient.HandleRedirects:=True;
    //aClient.Request.UserAgent:='';

    //can also test using an external web server, eg apache
    //aClient.Get('http://192.168.1.100:22280/helloworld.txt',aStream);
    aClient.Get('http://127.0.0.1:22280/'+cDocGZip,aStream);
    Assert(aStream.DataString='hello world',aClass.ClassName);
    //aStream.SaveToFile('e:\test.txt');
  finally
    Sys.FreeAndNil(aServer);
    Sys.FreeAndNil(aStream);
    Sys.FreeAndNil(aClient);
    Sys.FreeAndNil(aCompress);
  end;
end;

procedure TIdTestHTTP_GZip.TestGZip;
//basic gzip functionality test.
//doesn't really belong in this http unit
var
  aClass:TIdZLibCompressorBaseClass;
  aCompress:TIdZLibCompressorBase;
  //dont use stringstream as it acts differently
  aStream,aOutStream:TIdMemoryStream;
  s:string;
begin
  //string now contains a gz encoded test string
  s:=DecodeString(TIdDecoderMIME,cHelloWorldGz);

  //better way to do? eg iterate and test all registered compression classes?
  {$IFDEF DOTNET}
  aClass:=nil;
  {$ELSE}
    {$IFDEF INDY_USE_ABBREVIA}
    aClass:=TIdCompressorAbbrevia;
    {$ELSE}
    aClass:=TIdCompressorZLibEx;
    {$ENDIF}
  {$ENDIF}

  Assert(aClass<>nil);
  aStream:=TIdMemoryStream.Create;
  WriteStringToStream(aStream,s);
  aOutStream:=TIdMemoryStream.Create;
  aCompress:=aClass.Create;
  try
    aStream.Position:=0;
    aCompress.DecompressGZipStream(aStream,aOutStream);
    aOutStream.Position:=0;
    s:=ReadStringFromStream(aOutStream);
    Assert(s=cHelloWorld,s);
  finally
    Sys.FreeAndNil(aCompress);
    Sys.FreeAndNil(aStream);
    Sys.FreeAndNil(aOutStream);
  end;
end;
{$ENDIF}

procedure TIdTestHTTP_02.CallbackGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  if ARequestInfo.Document='/redirect' then
    begin
    AResponseInfo.ContentText:='message';
    AResponseInfo.Redirect('/download');
    end
  else if ARequestInfo.Document='/download' then
    begin
    AResponseInfo.ContentText:=cRedirectData;
    end;
end;

procedure TIdTestHTTP_02.SetUp;
begin
  inherited;
  FServer:=TIdHTTPServer.Create;
  FServer.DefaultPort:=22280;
  FServer.OnCommandGet:=Self.CallbackGet;

  FClient:=TIdHTTP.Create;
end;

procedure TIdTestHTTP_02.TearDown;
begin
  Sys.FreeAndNil(FServer);
  Sys.FreeAndNil(FClient);
  inherited;
end;

procedure TIdTestHTTP_02.TestRedirectedDownload;
//this shows that when requesting a download from a server, content
//from the redirecting page is not included in the data from the
//actual download
var
  s:TIdMemoryStream;
  aStr:string;
begin
  FServer.Active:=True;

  s:=TIdMemoryStream.Create;
  try
    FClient.HandleRedirects:=True;
    FClient.Get('http://127.0.0.1:22280/redirect?download',s);
    s.Position:=0;
    aStr:=ReadStringFromStream(s);
    Assert(aStr=cRedirectData);
  finally
    sys.FreeAndNil(s);
  end;
end;

initialization

  TIdTest.RegisterTest(TIdTestHTTP_01);
  TIdTest.RegisterTest(TIdTestHTTP_02);
  TIdTest.RegisterTest(TIdTestHTTP_KeepAlive);
  TIdTest.RegisterTest(TIdTestHTTP_Redirect);
  {$IFNDEF DOTNET}
  TIdTest.RegisterTest(TIdTestHTTP_GZip);
  {$ENDIF}
end.
