
{******************************************}
{                                          }
{             FastReport v4.0              }
{        HTTP connection client unit       }
{                                          }
{         Copyright (c) 1998-2007          }
{         by Alexander Fediachov,          }
{            Fast Reports Inc.             }
{                                          }
{******************************************}

unit frxHTTPClient;

{$I frx.inc}

interface

uses
  Windows, SysUtils, Classes, ScktComp, frxServerUtils, frxNetUtils,
  frxGzip, frxMD5
{$IFDEF Delphi6}
, Variants
{$ENDIF};

type
  TfrxHTTPServerFields = class;
  TfrxHTTPClientFields = class;
  TfrxClientThread = class;

  TfrxHTTPClient = class(TComponent)
  private
    FActive: Boolean;
    FAnswer: TStrings;
    FBreaked: Boolean;
    FClientFields: TfrxHTTPClientFields;
    FErrors: TStrings;
    FHeader: TStrings;
    FHost: String;
    FMIC: Boolean;
    FPort: Integer;
    FProxyHost: String;
    FProxyPort: Integer;
    FRetryCount: Integer;
    FRetryTimeOut: Integer;
    FServerFields: TfrxHTTPServerFields;
    FStream: TMemoryStream;
    FTempStream: TMemoryStream;
    FThread: TfrxClientThread;
    FTimeOut: Integer;
    FProxyLogin: String;
    FProxyPassword: String;
    procedure DoConnect(Sender: TObject; Socket: TCustomWinSocket);
    procedure DoDisconnect(Sender: TObject; Socket: TCustomWinSocket);
    procedure DoError(Sender: TObject; Socket: TCustomWinSocket;
      ErrorEvent: TErrorEvent; var ErrorCode: Integer);
    procedure DoRead(Sender: TObject; Socket: TCustomWinSocket);
    procedure SetActive(const Value: Boolean);
    procedure SetClientFields(const Value: TfrxHTTPClientFields);
    procedure SetServerFields(const Value: TfrxHTTPServerFields);
  public
    ParentThread: TThread;
    StreamSize: Cardinal;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Connect;
    procedure Disconnect;
    procedure Open;
    procedure Close;
    property Answer: TStrings read FAnswer write FAnswer;
    property Breaked: Boolean read FBreaked;
    property Errors: TStrings read FErrors write Ferrors;
    property Header: TStrings read FHeader write FHeader;
    property Stream: TMemoryStream read FStream write FStream;
  published
    property Active: Boolean read FActive write SetActive;
    property ClientFields: TfrxHTTPClientFields read FClientFields write SetClientFields;
    property Host: String read FHost write FHost;
    property MIC: Boolean read FMIC write FMIC;
    property Port: Integer read FPort write FPort;
    property ProxyHost: String read FProxyHost write FProxyHost;
    property ProxyPort: Integer read FProxyPort write FProxyPort;
    property ProxyLogin: String  read FProxyLogin write FProxyLogin;
    property ProxyPassword: String read FProxyPassword write FProxyPassword;
    property RetryCount: Integer read FRetryCount write FRetryCount;
    property RetryTimeOut: Integer read FRetryTimeOut write FRetryTimeOut;
    property ServerFields: TfrxHTTPServerFields read FServerFields write SetServerFields;
    property TimeOut: Integer read FTimeOut write FTimeOut;
  end;

  TfrxHTTPServerFields = class (TPersistent)
  private
    FAnswerCode: Integer;
    FContentEncoding: String;
    FContentMD5: String;
    FContentLength: Integer;
    FLocation: String;
    FSessionId: String;
    FCookie: String;
  public
    constructor Create;
    procedure Assign(Source: TPersistent); override;
  published
    property AnswerCode: Integer read FAnswerCode write FAnswerCode;
    property ContentEncoding: String read FContentEncoding write FContentEncoding;
    property ContentMD5: String read FContentMD5 write FContentMD5;
    property ContentLength: Integer read FContentLength write FContentLength;
    property Location: String read FLocation write FLocation;
    property SessionId: String read FSessionId write FSessionId;
    property Cookie: String read FCookie write FCookie;
  end;

  TfrxHTTPClientFields = class (TPersistent)
  private
    FAcceptEncoding: String;
    FHost: String;
    FHTTPVer: String;
    FLogin: String;
    FName: String;
    FPassword: String;
    FQueryType: TfrxHTTPQueryType;
    FReferer: String;
    FUserAgent: String;
    FVariables: String;
    FAccept: String;
    FAcceptCharset: String;
    FContentType: String;
    FRange: String;
  public
    constructor Create;
    procedure Assign(Source: TPersistent); override;
  published
    property AcceptEncoding: String read FAcceptEncoding write FAcceptEncoding;
    property Accept: String read FAccept write FAccept;
    property AcceptCharset: String read FAcceptCharset write FAcceptCharset;
    property FileName: String read FName write FName;
    property Host: String read FHost write FHost;
    property HTTPVer: String read FHTTPVer write FHTTPVer;
    property Login: String read FLogin write FLogin;
    property Password: String read FPassword write FPassword;
    property QueryType: TfrxHTTPQueryType read FQueryType write FQueryType;
    property Referer: String read FReferer write FReferer;
    property UserAgent: String read FUserAgent write FUserAgent;
    property Variables: String read FVariables write FVariables;
    property ContentType: String read FContentType write FContentType;
    property Range: String read FRange write FRange;
  end;

  TfrxClientThread = class (TThread)
  protected
    FClient: TfrxHTTPClient;
    procedure DoOpen;
    procedure Execute; override;
  public
    FSocket: TClientSocket;
    constructor Create(Client: TfrxHTTPClient);
    destructor Destroy; override;
  end;

implementation

uses frxFileUtils;

type
  THackThread = class(TThread);

{ TfrxHTTPServerFields }

constructor TfrxHTTPServerFields.Create;
begin
  FAnswerCode := 0;
  FLocation := '';
  FContentEncoding := '';
  FContentMD5 := '';
  FContentLength := 0;
end;

procedure TfrxHTTPServerFields.Assign(Source: TPersistent);
begin
  if Source is TfrxHTTPServerFields then
  begin
    FAnswerCode := TfrxHTTPServerFields(Source).AnswerCode;
    FLocation := TfrxHTTPServerFields(Source).Location;
    FContentEncoding := TfrxHTTPServerFields(Source).ContentEncoding;
    FContentMD5 := TfrxHTTPServerFields(Source).ContentMD5;
    FContentLength := TfrxHTTPServerFields(Source).ContentLength;
  end;
end;

{ TfrxHTTPClientFields }

constructor TfrxHTTPClientFields.Create;
begin
  FQueryType := qtGet;
  FHTTPVer := 'HTTP/1.0';
  FName := '';
  FUserAgent := 'FastReport/4.0';
  FHost := '';
  FAcceptEncoding := 'gzip';
  FLogin := '';
  FPassword := '';
  FReferer := '';
end;

procedure TfrxHTTPClientFields.Assign(Source: TPersistent);
begin
  if Source is TfrxHTTPClientFields then
  begin
    FQueryType := TfrxHTTPClientFields(Source).QueryType;
    FName := TfrxHTTPClientFields(Source).FileName;
    FHTTPVer := TfrxHTTPClientFields(Source).HTTPVer;
    FUserAgent := TfrxHTTPClientFields(Source).UserAgent;
    FHost := TfrxHTTPClientFields(Source).Host;
    FAcceptEncoding := TfrxHTTPClientFields(Source).AcceptEncoding;
    FLogin := TfrxHTTPClientFields(Source).Login;
    FPassword := TfrxHTTPClientFields(Source).Password;
    FReferer := TfrxHTTPClientFields(Source).Referer;
  end;
end;

{ TfrxHTTPClient }

constructor TfrxHTTPClient.Create(AOwner: TComponent);
begin
  inherited;
  FHeader := TStringList.Create;
  FAnswer := TStringList.Create;
  FStream := TMemoryStream.Create;
  FTempStream := TMemoryStream.Create;
  FErrors := TStringList.Create;
  FHost := '127.0.0.1';
  FPort := 80;
  FProxyHost := '';
  FProxyPort := 8080;
  FActive := False;
  FServerFields := TfrxHTTPServerFields.Create;
  FClientFields := TfrxHTTPClientFields.Create;
  FRetryTimeOut := 5;
  FRetryCount := 3;
  FTimeOut := 30;
  FBreaked := False;
  FMIC := True;
  ParentThread := nil;
  FThread := TfrxClientThread.Create(Self);
  FThread.FSocket.OnConnect := DoConnect;
  FThread.FSocket.OnRead := DoRead;
  FThread.FSocket.OnDisconnect := DoDisconnect;
  FThread.FSocket.OnError := DoError;
end;

destructor TfrxHTTPClient.Destroy;
begin
  Close;
  PMessages;
  while FActive do
    PMessages;
  FThread.Free;
  FClientFields.Free;
  FServerFields.Free;
  FHeader.Free;
  FAnswer.Free;
  FStream.Free;
  FTempStream.Free;
  FErrors.Free;
  inherited;
end;

procedure TfrxHTTPClient.Connect;
var
  ticks: Cardinal;
  i: Integer;
begin
  i := FRetryCount;
  FBreaked := False;
  repeat
    FErrors.Clear;
    FTempStream.Clear;
    FActive := True;
    if Length(FProxyHost) > 0 then
    begin
      FThread.FSocket.Host := FProxyHost;
      FThread.FSocket.Address := FProxyHost;
      FThread.FSocket.Port := FProxyPort;
    end else
    begin
      FThread.FSocket.Host := FHost;
      FThread.FSocket.Address := FHost;
      FThread.FSocket.Port := FPort;
    end;
    FThread.FSocket.ClientType := ctNonBlocking;
    FThread.Execute;
    try
      ticks := GetTickCount;
      while FActive and (not FBreaked) do
      begin
        PMessages;
        if (GetTickCount - ticks) > Cardinal(FTimeOut * 1000) then
        begin
          Errors.Add('Timeout expired (' + IntToStr(FTimeOut) + ')');
          break;
        end;
      end;
    finally
      Disconnect;
    end;
    if not FBreaked then
    begin
      if (Errors.Count = 0) and ((FServerFields.AnswerCode = 301) or
         (FServerFields.AnswerCode = 302) or (FServerFields.AnswerCode = 303)) then
      begin
        i := FRetryCount;
        FClientFields.FileName := FServerFields.Location;
      end
      else if (Errors.Count > 0)
          and (FServerFields.AnswerCode <> 500)
          and (FServerFields.AnswerCode <> 401)
          and (FServerFields.AnswerCode <> 403)
          and (FServerFields.AnswerCode <> 404) then
      begin
        Dec(i);
        if i > 0 then
          Sleep(FRetryTimeOut * 1000)
        else
          if FRetryCount > 1 then
            Errors.Add('Retry count (' + IntToStr(FRetryCount) + ') exceed')
      end else
        i := 0;
    end;
  until (i = 0) or FBreaked;
end;

procedure TfrxHTTPClient.Disconnect;
begin
//  Close;
  FThread.FSocket.Close;
//  FThread.Terminate;
//  if not FThread.Terminated then
//    FThread.WaitFor;
  FActive := False;
end;

procedure TfrxHTTPClient.DoConnect(Sender: TObject;
  Socket: TCustomWinSocket);
var
  s, s1, s2: String;
  m: TMemoryStream;
begin
  FHeader.Clear;
  if FClientFields.QueryType = qtGet then
    s := 'GET'
  else if FClientFields.QueryType = qtPost then
    s := 'POST'
  else if FClientFields.QueryType = qtHead then
    s := 'HEAD'
  else
    s := '';
  if Length(FProxyHost) > 0 then
    s1 := 'http://' + Host + ':' + IntToStr(FPort) + '/' + FClientFields.FileName
  else
  begin
    if ((Length(FClientFields.FileName) > 0) and (FClientFields.FileName[1] = '/')) or (Pos('http://', FClientFields.FileName) = 1) then
      s1 := FClientFields.FileName
    else
      s1 := '/' + FClientFields.FileName;
  end;
  s2 := FClientFields.Variables;
  if (FClientFields.QueryType = qtGet) and (s2 <> '') then
    s1 := s1 + '?' + s2;
  FHeader.Add(s + ' ' + s1 + ' ' + FClientFields.HTTPVer);
  if Length(FClientFields.Host) = 0 then
    s := Socket.LocalAddress
  else
    s := FClientFields.Host;
  FHeader.Add('Host: ' + Host);
  if Length(FClientFields.UserAgent) > 0 then
    FHeader.Add('User-Agent: ' + FClientFields.UserAgent);
  if FClientFields.Accept <> '' then
    FHeader.Add('Accept: ' + FClientFields.Accept);
  if Length(FClientFields.AcceptEncoding) > 0 then
    FHeader.Add('Accept-Encoding: ' + FClientFields.AcceptEncoding);
  if FClientFields.AcceptCharset <> '' then
    FHeader.Add('Accept-Charset: ' + FClientFields.AcceptCharset);
  if (FProxyHost <> '') and (FProxyLogin <> '') then
    FHeader.Add('Proxy-Authorization: Basic ' + Base64Encode(FProxyLogin + ':' +
      FproxyPassword));
  if Length(FClientFields.Login) > 0 then
    FHeader.Add('Authorization: Basic ' + Base64Encode(FClientFields.Login + ':' +
      FClientFields.Password));
  FHeader.Add('Connection: close');
  if FClientFields.Referer <> '' then
    FHeader.Add('Referer: ' + FClientFields.Referer);
  if FClientFields.ContentType <> '' then
    FHeader.Add('Content-Type: ' + FClientFields.ContentType);
  if FServerFields.Cookie <> '' then
    FHeader.Add('Cookie: ' + FServerFields.Cookie);
  if FClientFields.Range <> '' then
    FHeader.Add('Range: ' + FClientFields.Range);
  if (FClientFields.QueryType = qtPost) and (s2 <> '') then
  begin
    FStream.Write(s2[1], Length(s2));
    FStream.Position := 0;
  end;
  if FStream.Size > 0 then
    FHeader.Add('Content-Length: ' + IntToStr(FStream.Size));
  FHeader.Add('');
  try
    m := TMemoryStream.Create;
    try
      m.Write(FHeader.Text[1], Length(FHeader.Text));
      if FStream.Size > 1 then
        m.Write(FStream.Memory^, FStream.Size);
      Socket.SendBuf(m.Memory^, m.Size);
    finally
      m.Free;
    end
  except
    Errors.Add('Data send error');
  end;
end;

procedure TfrxHTTPClient.DoDisconnect(Sender: TObject;
  Socket: TCustomWinSocket);
var
  i, j, Len: Integer;
  s, s1, s2: String;
  MICStream: TMemoryStream;

  function IsDigit(const c: Char): Boolean;
  begin
    Result := c in ['0'..'9'];
  end;

begin
  FAnswer.Clear;
  FStream.Clear;
  if FTempStream.Size > 0 then
  begin
    FTempStream.Position := 0;
    i := StreamSearch(FTempStream, 0, #13#10#13#10);
    if i <> 0 then
    begin
      Len := i + 4;
      StreamSize := FTempStream.Size - Len;
      SetLength(s, Len);
      FTempStream.Position := 0;
      FTempStream.ReadBuffer(s[1], Len);
      FAnswer.Text := s;

      i := Pos(#13#10, s);
      s1 := Copy(s, 1, i - 1);
      j := 0;
      s2 := '';
      for i := 1 to Length(s1) do
      begin
        if IsDigit(s1[i]) then
        begin
          s2 := s2 + s1[i];
          Inc(j);
        end else
        if j = 3 then
          break
        else
        begin
          j := 0;
          s2 := '';
        end;
      end;
      s1 := s2;

      if Length(s1) = 3 then
        FServerFields.FAnswerCode := StrToInt(s1);
      s1 := ParseHeaderField('Location: ', s);
      if (Length(s1) > 0) and (s1[1] = '/') then
        Delete(s1, 1, 1);
      FServerFields.Location := s1;
      FServerFields.ContentEncoding := LowerCase(ParseHeaderField('Content-Encoding: ', s));
      FServerFields.ContentMD5 := ParseHeaderField('Content-MD5: ', s);
      s1 := ParseHeaderField('Set-Cookie: ', s);
      if s1 <> '' then
        FServerFields.Cookie := s1;
      s1 := ParseHeaderField('SessionId: ', s);
      if Length(s1) > 0 then
        FServerFields.SessionId := s1;
      s1 := ParseHeaderField('Content-length: ', s);
      if Length(s1) > 0 then
        FServerFields.ContentLength := StrToInt(s1);
      s1 := GetHTTPErrorText(FServerFields.AnswerCode);
      if Length(s1) > 0 then
        Errors.Add(s1);
      if Errors.Count = 0 then
      begin
        if FServerFields.ContentLength > 0 then
          if ((FTempStream.Size - Len) <> FServerFields.ContentLength) and ((FServerFields.FAnswerCode = 200) or (FServerFields.FAnswerCode = 206)) then
            Errors.Add('Received data size mismatch');
        if (Length(FServerFields.ContentMD5) > 0) and FMIC and (Errors.Count = 0) then
        begin
          MICStream := TMemoryStream.Create;
          try
            MICStream.CopyFrom(FTempStream, FTempStream.Size - Len);
            if MD5Stream(MICStream) <> FServerFields.ContentMD5 then
              Errors.Add('Message integrity checksum (MIC) error');
          finally
            FTempStream.Position := Len;
            MICStream.Free;
          end;
        end;
        if Errors.Count = 0 then
          if Pos('gzip', FServerFields.ContentEncoding) > 0 then
          try
            frxDecompressStream(FTempStream, FStream)
          except
            Errors.Add('Unpack data error')
          end
          else
            FStream.CopyFrom(FTempStream, FTempStream.Size - Len);
      end;
    end else
      Errors.Add('Bad header');
    FTempStream.Clear;
  end
  else if Errors.Count = 0 then
    Errors.Add('Zero bytes received');
  if FStream.Size > 0 then
    FStream.Position := 0;
  FActive := False;
end;

procedure TfrxHTTPClient.DoError(Sender: TObject; Socket: TCustomWinSocket;
  ErrorEvent: TErrorEvent; var ErrorCode: Integer);
begin
  Errors.Add(GetSocketErrorText(ErrorCode));
  FActive := False;
  ErrorCode := 0;
end;

procedure TfrxHTTPClient.DoRead(Sender: TObject; Socket: TCustomWinSocket);
var
  buf: PChar;
  i, j: Integer;
begin
  i := Socket.ReceiveLength;
  GetMem(buf, i);
  j := i;
  try
    try
      while j > 0 do
      begin
        j := Socket.ReceiveBuf(buf^, i);
        FTempStream.Write(buf^, j);
      end;
    except
      Errors.Add('Data receive error.')
    end;
  finally
    FreeMem(buf);
  end;
end;

procedure TfrxHTTPClient.SetActive(const Value: Boolean);
begin
  if Value then Connect
  else Disconnect;
end;

procedure TfrxHTTPClient.Close;
begin
  FBreaked := True;
  Active := False;
end;

procedure TfrxHTTPClient.Open;
begin
  Active := True;
end;

procedure TfrxHTTPClient.SetServerFields(const Value: TfrxHTTPServerFields);
begin
  FServerFields.Assign(Value);
end;

procedure TfrxHTTPClient.SetClientFields(const Value: TfrxHTTPClientFields);
begin
  FClientFields.Assign(Value);
end;

{ TfrxClientThread }

constructor TfrxClientThread.Create(Client: TfrxHTTPClient);
begin
  inherited Create(True);
  FClient := Client;
  FreeOnTerminate := False;
  FSocket := TClientSocket.Create(nil);
end;

destructor TfrxClientThread.Destroy;
begin
  FSocket.Close;
  FSocket.Free;
  inherited;
end;

procedure TfrxClientThread.DoOpen;
begin
//
end;

procedure TfrxClientThread.Execute;
begin
  FSocket.Open;
//  DoOpen;
end;

end.
