{ lFTP CopyRight (C) 2005-2008 Ales Katona

  This library is Free software; you can rediStribute it and/or modify it
  under the terms of the GNU Library General Public License as published by
  the Free Software Foundation; either version 2 of the License, or (at your
  option) any later version.

  This program is diStributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; withOut even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
  for more details.

  You should have received a Copy of the GNU Library General Public License
  along with This library; if not, Write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

  This license has been modified. See File LICENSE for more inFormation.
  Should you find these sources withOut a LICENSE File, please contact
  me at ales@chello.sk
}

unit lFTP;

{$mode objfpc}{$H+}
{$inline on}
{$macro on}
//{$define debug}

interface

uses
  Classes, lNet, lTelnet;
  
const
  DEFAULT_FTP_PORT    = 1025;

type
  TLFTP = class;
  TLFTPClient = class;

  TLFTPStatus = (fsNone, fsCon, fsUser, fsPass, fsPasv, fsPort, fsList, fsRetr,
                 fsStor, fsType, fsCWD, fsMKD, fsRMD, fsDEL, fsRNFR, fsRNTO,
                 fsSYS, fsFeat, fsPWD, fsHelp, fsLast);
                 
  TLFTPStatusSet = set of TLFTPStatus;
                 
  TLFTPStatusRec = record
    Status: TLFTPStatus;
    Args: array[1..2] of string;
  end;
  
  TLFTPTransferMethod = (ftActive, ftPassive);
                 
  TLFTPClientStatusEvent = procedure (aSocket: TLSocket;
                                     const aStatus: TLFTPStatus) of object;

  { TLFTPStatusStack }

  { TLFTPStatusFront }
  {$DEFINE __front_type__  :=  TLFTPStatusRec}
  {$i lcontainersh.inc}
  TLFTPStatusFront = TLFront;
  
  TLFTP = class(TLComponent, ILDirect)
   protected
    FControl: TLTelnetClient;
    FData: TLTcp;//TLTcpList;
    FSending: Boolean;
    FTransferMethod: TLFTPTransferMethod;
    FFeatureList: TStringList;
    FFeatureString: string;

    function GetConnected: Boolean; virtual;
    
    function GetTimeout: Integer;
    procedure SetTimeout(const Value: Integer);

    function GetSession: TLSession;
    procedure SetSession(const AValue: TLSession);
    procedure SetCreator(AValue: TLComponent); override;

    function GetSocketClass: TLSocketClass;
    procedure SetSocketClass(Value: TLSocketClass);
   public
    constructor Create(aOwner: TComponent); override;
    destructor Destroy; override;
    
    function Get(out aData; const aSize: Integer; aSocket: TLSocket = nil): Integer; virtual; abstract;
    function GetMessage(out msg: string; aSocket: TLSocket = nil): Integer; virtual; abstract;
    
    function Send(const aData; const aSize: Integer; aSocket: TLSocket = nil): Integer; virtual; abstract;
    function SendMessage(const msg: string; aSocket: TLSocket = nil): Integer; virtual; abstract;
    
   public
    property Connected: Boolean read GetConnected;
    property Timeout: Integer read GetTimeout write SetTimeout;
    property SocketClass: TLSocketClass read GetSocketClass write SetSocketClass;
    property ControlConnection: TLTelnetClient read FControl;
    property DataConnection: TLTCP read FData;
    property TransferMethod: TLFTPTransferMethod read FTransferMethod write FTransferMethod default ftPassive;
    property Session: TLSession read GetSession write SetSession;
    property FeatureList: TStringList read FFeatureList;
  end;

  { TLFTPTelnetClient }
  
  TLFTPTelnetClient = class(TLTelnetClient)
   protected
    function React(const Operation, Command: Char):boolean; override;
  end;

  { TLFTPClient }

  TLFTPClient = class(TLFTP, ILClient)
   protected
    FStatus: TLFTPStatusFront;
    FCommandFront: TLFTPStatusFront;
    FStoreFile: TFileStream;
    FExpectedBinary: Boolean;
    FPipeLine: Boolean;
    FPassword: string;
    FPWD: string;
    FStatusFlags: array[TLFTPStatus] of Boolean;

    FOnError: TLSocketErrorEvent;
    FOnReceive: TLSocketEvent;
    FOnSent: TLSocketProgressEvent;
    FOnControl: TLSocketEvent;
    FOnConnect: TLSocketEvent;
    FOnSuccess: TLFTPClientStatusEvent;
    FOnFailure: TLFTPClientStatusEvent;

    FChunkSize: Word;
    FLastPort: Word;
    FStartPort: Word;
    FStatusSet: TLFTPStatusSet;
    FSL: TStringList; // for evaluation, I want to prevent constant create/free
    procedure OnRe(aSocket: TLSocket);
    procedure OnDs(aSocket: TLSocket);
    procedure OnSe(aSocket: TLSocket);
    procedure OnEr(const msg: string; aSocket: TLSocket);

    procedure OnControlEr(const msg: string; aSocket: TLSocket);
    procedure OnControlRe(aSocket: TLSocket);
    procedure OnControlCo(aSocket: TLSocket);
    procedure OnControlDs(aSocket: TLSocket);
    
    procedure ClearStatusFlags;

    function GetCurrentStatus: TLFTPStatus;
    function GetTransfer: Boolean;

    function GetEcho: Boolean;
    procedure SetEcho(const Value: Boolean);

    procedure ParsePWD(const s: string);

    function GetConnected: Boolean; override;

    function GetBinary: Boolean;
    procedure SetBinary(const Value: Boolean);

    function CanContinue(const aStatus: TLFTPStatus; const Arg1, Arg2: string): Boolean;

    function CleanInput(var s: string): Integer;

    procedure SetStartPor(const Value: Word);

    procedure EvaluateFeatures;
    procedure EvaluateAnswer(const Ans: string);

    procedure PasvPort;

    function User(const aUserName: string): Boolean;
    function Password(const aPassword: string): Boolean;

    procedure SendChunk(const Event: Boolean);

    procedure ExecuteFrontCommand;
   public
    constructor Create(aOwner: TComponent); override;
    destructor Destroy; override;

    function Get(out aData; const aSize: Integer; aSocket: TLSocket = nil): Integer; override;
    function GetMessage(out msg: string; aSocket: TLSocket = nil): Integer; override;
    
    function Send(const aData; const aSize: Integer; aSocket: TLSocket = nil): Integer; override;
    function SendMessage(const msg: string; aSocket: TLSocket = nil): Integer; override;
    
    function Connect(const aHost: string; const aPort: Word = 21): Boolean; virtual; overload;
    function Connect: Boolean; virtual; overload;
    
    function Authenticate(const aUsername, aPassword: string): Boolean;
    
    function GetData(out aData; const aSize: Integer): Integer;
    function GetDataMessage: string;
    
    function Retrieve(const FileName: string): Boolean;
    function Put(const FileName: string): Boolean; virtual; // because of LCLsocket
    
    function ChangeDirectory(const DestPath: string): Boolean;
    function MakeDirectory(const DirName: string): Boolean;
    function RemoveDirectory(const DirName: string): Boolean;
    
    function DeleteFile(const FileName: string): Boolean;
    function Rename(const FromName, ToName: string): Boolean;
   public
    procedure List(const FileName: string = '');
    procedure Nlst(const FileName: string = '');
    procedure SystemInfo;
    procedure ListFeatures;
    procedure PresentWorkingDirectory;
    procedure Help(const Arg: string);
    
    procedure Disconnect(const Forced: Boolean = True); override;
    
    procedure CallAction; override;
   public
    property StatusSet: TLFTPStatusSet read FStatusSet write FStatusSet;
    property ChunkSize: Word read FChunkSize write FChunkSize;
    property Binary: Boolean read GetBinary write SetBinary;
    property PipeLine: Boolean read FPipeLine write FPipeLine;
    property Echo: Boolean read GetEcho write SetEcho;
    property StartPort: Word read FStartPort write FStartPort default DEFAULT_FTP_PORT;
    property Transfer: Boolean read GetTransfer;
    property CurrentStatus: TLFTPStatus read GetCurrentStatus;
    property PresentWorkingDirectoryString: string read FPWD;

    property OnError: TLSocketErrorEvent read FOnError write FOnError;
    property OnConnect: TLSocketEvent read FOnConnect write FOnConnect;
    property OnSent: TLSocketProgressEvent read FOnSent write FOnSent;
    property OnReceive: TLSocketEvent read FOnReceive write FOnReceive;
    property OnControl: TLSocketEvent read FOnControl write FOnControl;
    property OnSuccess: TLFTPClientStatusEvent read FOnSuccess write FOnSuccess;
    property OnFailure: TLFTPClientStatusEvent read FOnFailure write FOnFailure;
  end;
  
  function FTPStatusToStr(const aStatus: TLFTPStatus): string;
  
implementation

uses
  SysUtils, Math;

const
  FLE             = #13#10;

  EMPTY_REC: TLFTPStatusRec = (Status: fsNone; Args: ('', ''));

  FTPStatusStr: array[TLFTPStatus] of string = ('None', 'Connect', 'Authenticate', 'Password',
                                                'Passive', 'Active', 'List', 'Retrieve',
                                                'Store', 'Type', 'CWD', 'MKDIR',
                                                'RMDIR', 'Delete', 'RenameFrom',
                                                'RenameTo', 'System', 'Features',
                                                'PWD', 'HELP', 'LAST');

procedure Writedbg(const ar: array of const);
{$ifdef debug}
var
  i: Integer;
begin
  if High(ar) >= 0 then
    for i := 0 to High(ar) do
      case ar[i].vtype of
        vtInteger: Write(ar[i].vinteger);
        vtString: Write(ar[i].vstring^);
        vtAnsiString: Write(AnsiString(ar[i].vpointer));
        vtBoolean: Write(ar[i].vboolean);
        vtChar: Write(ar[i].vchar);
        vtExtended: Write(Extended(ar[i].vpointer^));
      end;
  Writeln;
end;
{$else}
begin
end;
{$endif}

function MakeStatusRec(const aStatus: TLFTPStatus; const Arg1, Arg2: string): TLFTPStatusRec;
begin
  Result.Status := aStatus;
  Result.Args[1] := Arg1;
  Result.Args[2] := Arg2;
end;

function FTPStatusToStr(const aStatus: TLFTPStatus): string;
begin
  Result := FTPStatusStr[aStatus];
end;

{$i lcontainers.inc}

{ TLFTP }

function TLFTP.GetSession: TLSession;
begin
  Result := FControl.Session;
end;

procedure TLFTP.SetSession(const AValue: TLSession);
begin
  FControl.Session := aValue;
  FData.Session := aValue;
end;

procedure TLFTP.SetCreator(AValue: TLComponent);
begin
  inherited SetCreator(AValue);
  
  FControl.Creator := AValue;
  FData.Creator := AValue;
end;

function TLFTP.GetConnected: Boolean;
begin
  Result := FControl.Connected;
end;

function TLFTP.GetTimeout: Integer;
begin
  Result := FControl.Timeout;
end;

procedure TLFTP.SetTimeout(const Value: Integer);
begin
  FControl.Timeout := Value;
  FData.Timeout := Value;
end;

function TLFTP.GetSocketClass: TLSocketClass;
begin
  Result := FControl.SocketClass;
end;

procedure TLFTP.SetSocketClass(Value: TLSocketClass);
begin
  FControl.SocketClass := Value;
  FData.SocketClass := Value;
end;

constructor TLFTP.Create(aOwner: TComponent);
begin
  inherited Create(aOwner);

  FHost := '';
  FPort := 21;

  FControl := TLFTPTelnetClient.Create(nil);
  FControl.Creator := Self;

  FData := TLTcp.Create(nil);
  FData.Creator := Self;
  FData.SocketClass := TLSocket;

  FTransferMethod  :=  ftPassive; // let's be modern

  FFeatureList := TStringList.Create;
end;

destructor TLFTP.Destroy;
begin
  FControl.Free;
  FData.Free;
  FFeatureList.Free;

  inherited Destroy;
end;

{ TLFTPTelnetClient }

function TLFTPTelnetClient.React(const Operation, Command: Char):boolean;
begin
  result:=false;
  // don't do a FUCK since they broke Telnet in FTP as per-usual
end;

{ TLFTPClient }

constructor TLFTPClient.Create(aOwner: TComponent);
const
  DEFAULT_CHUNK = 8192;
begin
  inherited Create(aOwner);

  FControl.OnReceive := @OnControlRe;
  FControl.OnConnect := @OnControlCo;
  FControl.OnError := @OnControlEr;
  FControl.OnDisconnect := @OnControlDs;

  FData.OnReceive := @OnRe;
  FData.OnDisconnect := @OnDs;
  FData.OnCanSend := @OnSe;
  FData.OnError := @OnEr;

  FStatusSet := [fsNone..fsLast]; // full Event set
  FPassWord := '';
  FChunkSize := DEFAULT_CHUNK;
  FStartPort := DEFAULT_FTP_PORT;
  FSL := TStringList.Create;
  FLastPort := FStartPort;

  ClearStatusFlags;

  FStatus := TLFTPStatusFront.Create(EMPTY_REC);
  FCommandFront := TLFTPStatusFront.Create(EMPTY_REC);
  
  FStoreFile := nil;
end;

destructor TLFTPClient.Destroy;
begin
  Disconnect(True);
  FSL.Free;
  FStatus.Free;
  FCommandFront.Free;
  if Assigned(FStoreFile) then
    FreeAndNil(FStoreFile);
  inherited Destroy;
end;

procedure TLFTPClient.OnRe(aSocket: TLSocket);
begin
  if Assigned(FOnReceive) then
    FOnReceive(aSocket);
end;

procedure TLFTPClient.OnDs(aSocket: TLSocket);
begin
  FSending := False;
  Writedbg(['Disconnected']);
end;

procedure TLFTPClient.OnSe(aSocket: TLSocket);
begin
  if Connected and FSending then
    SendChunk(True);
end;

procedure TLFTPClient.OnEr(const msg: string; aSocket: TLSocket);
begin
  FSending := False;
  if Assigned(FOnError) then
    FOnError(msg, aSocket);
end;

procedure TLFTPClient.OnControlEr(const msg: string; aSocket: TLSocket);
begin
  FSending := False;
  
  if Assigned(FOnFailure) then begin
    while not FStatus.Empty do
      FOnFailure(aSocket, FStatus.Remove.Status);
  end else
    FStatus.Clear;
    
  ClearStatusFlags;

  if Assigned(FOnError) then
    FOnError(msg, aSocket);
end;

procedure TLFTPClient.OnControlRe(aSocket: TLSocket);
begin
  if Assigned(FOnControl) then
    FOnControl(aSocket);
end;

procedure TLFTPClient.OnControlCo(aSocket: TLSocket);
begin
  if Assigned(FOnConnect) then
    FOnConnect(aSocket);
end;

procedure TLFTPClient.OnControlDs(aSocket: TLSocket);
begin
  if Assigned(FOnError) then
    FOnError('Connection lost', aSocket);
end;

procedure TLFTPClient.ClearStatusFlags;
var
  s: TLFTPStatus;
begin
  for s := fsNone to fsLast do
    FStatusFlags[s] := False;
end;

function TLFTPClient.GetCurrentStatus: TLFTPStatus;
begin
  Result := FStatus.First.Status;
end;

function TLFTPClient.GetTransfer: Boolean;
begin
  Result := FData.Connected;
end;

function TLFTPClient.GetEcho: Boolean;
begin
  Result := FControl.OptionIsSet(TS_ECHO);
end;

function TLFTPClient.GetConnected: Boolean;
begin
  Result := FStatusFlags[fsCon] and inherited;
end;

function TLFTPClient.GetBinary: Boolean;
begin
  Result := FStatusFlags[fsType];
end;

function TLFTPClient.CanContinue(const aStatus: TLFTPStatus; const Arg1,
  Arg2: string): Boolean;
begin
  Result := FPipeLine or FStatus.Empty;
  if not Result then
    FCommandFront.Insert(MakeStatusRec(aStatus, Arg1, Arg2));
end;

function TLFTPClient.CleanInput(var s: string): Integer;
var
  i: Integer;
begin
  FSL.Text := s;
  for i := 0 to FSL.Count - 1 do
    if Length(FSL[i]) > 0 then
      EvaluateAnswer(FSL[i]);

  s := StringReplace(s, FLE, LineEnding, [rfReplaceAll]);
  i := Pos('PASS', s);
  if i > 0 then
    s := Copy(s, 1, i-1) + 'PASS';
  Result := Length(s);
end;

procedure TLFTPClient.SetStartPor(const Value: Word);
begin
  FStartPort := Value;
  if Value > FLastPort then
    FLastPort := Value;
end;

procedure TLFTPClient.EvaluateFeatures;
var
  i: Integer;
begin
  FFeatureList.Clear;
  if Length(FFeatureString) = 0 then
    Exit;

  FFeatureList.Text := FFeatureString;
  FFeatureString := '';
  FFeatureList.Delete(0);

  i := 0;
  while i < FFeatureList.Count do begin
    if (Length(Trim(FFeatureList[i])) = 0)
    or (FFeatureList[i][1] <> ' ') then begin
      FFeatureList.Delete(i);
      Continue;
    end;

    FFeatureList[i] := Trim(FFeatureList[i]);

    Inc(i);
  end;
end;

procedure TLFTPClient.SetEcho(const Value: Boolean);
begin
  if Value then
    FControl.SetOption(TS_ECHO)
  else
    FControl.UnSetOption(TS_ECHO);
end;

procedure TLFTPClient.ParsePWD(const s: string);
var
  i: Integer;
  IsIn: Boolean = False;
begin
  FPWD := '';
  for i := 1 to Length(s) do begin
    if s[i] = '"' then begin
      IsIn := not IsIn;
      Continue;
    end;
    if IsIn then
      FPWD := FPWD + s[i];
  end;
end;

procedure TLFTPClient.SetBinary(const Value: Boolean);
const
  TypeBool: array[Boolean] of string = ('A', 'I');
begin
  if CanContinue(fsType, BoolToStr(Value), '') then begin
    FExpectedBinary := Value;
    FStatus.Insert(MakeStatusRec(fsType, '', ''));
    FControl.SendMessage('TYPE ' + TypeBool[Value] + FLE);
  end;
end;

procedure TLFTPClient.EvaluateAnswer(const Ans: string);

  function GetNum: Integer;
  begin
    Result := -1;
    if (Length(Ans) >= 3)
    and (Ans[1] in ['0'..'9'])
    and (Ans[2] in ['0'..'9'])
    and (Ans[3] in ['0'..'9']) then
      Result := StrToInt(Copy(Ans, 1, 3));
  end;

  procedure ParsePortIP(s: string);
  var
    i, l: Integer;
    aIP: string;
    aPort: Word;
    sl: TStringList;
  begin
    if Length(s) >= 15 then begin
      sl := TStringList.Create;
      for i := Length(s) downto 5 do
        if s[i] = ',' then Break;
      while (i <= Length(s)) and (s[i] in ['0'..'9', ',']) do Inc(i);
      if not (s[i] in ['0'..'9', ',']) then Dec(i);
      l := 0;
      while s[i] in ['0'..'9', ','] do begin
        Inc(l);
        Dec(i);
      end;
      Inc(i);
      s := Copy(s, i, l);
      sl.CommaText := s;
      aIP := sl[0] + '.' + sl[1] + '.' + sl[2] + '.' + sl[3];
      try
        aPort := (StrToInt(sl[4]) * 256) + StrToInt(sl[5]);
      except
        aPort := 0;
      end;
      Writedbg(['Server PASV addr/port - ', aIP, ' : ', aPort]);
      if (aPort > 0) and FData.Connect(aIP, aPort) then
        Writedbg(['Connected after PASV']);
      sl.Free;
      FStatus.Remove;
    end;
  end;
  
  procedure SendFile;
  begin
    FStoreFile.Position := 0;
    FSending := True;
    SendChunk(False);
  end;
  
  function ValidResponse(const Answer: string): Boolean; inline;
  begin
    Result := (Length(Ans) >= 3) and
            (Ans[1] in ['1'..'5']) and
            (Ans[2] in ['0'..'9']) and
            (Ans[3] in ['0'..'9']);
            
    if Result then
      Result := (Length(Ans) = 3) or ((Length(Ans) > 3) and (Ans[4] = ' '));
  end;
  
  procedure Eventize(const aStatus: TLFTPStatus; const Res: Boolean);
  begin
    FStatus.Remove;
    if Res then begin
      if Assigned(FOnSuccess) and (aStatus in FStatusSet) then
        FOnSuccess(FData.Iterator, aStatus);
    end else begin
      if Assigned(FOnFailure) and (aStatus in FStatusSet) then
        FOnFailure(FData.Iterator, aStatus);
    end;
  end;
  
var
  x: Integer;
begin
  x := GetNum;
  Writedbg(['WOULD EVAL: ', FTPStatusStr[FStatus.First.Status], ' with value: ',
            x, ' from "', Ans, '"']);
  if FStatus.First.Status = fsFeat then
    FFeatureString := FFeatureString + Ans + FLE; // we need to parse this later

  if ValidResponse(Ans) then
    if not FStatus.Empty then begin
      Writedbg(['EVAL: ', FTPStatusStr[FStatus.First.Status], ' with value: ', x]);
      case FStatus.First.Status of
        fsCon  : case x of
                   220:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsUser : case x of
                   230:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   331,
                   332:
                     begin
                       FStatus.Remove;
                       Password(FPassword);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
                 
        fsPass : case x of
                   230:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsPasv : case x of
                   227: ParsePortIP(Ans);
                   300..600: FStatus.Remove;
                 end;

        fsPort : case x of
                   200:
                     begin
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsType : case x of
                   200:
                     begin
                       FStatusFlags[FStatus.First.Status] := FExpectedBinary;
                       Writedbg(['Binary mode: ', FExpectedBinary]);
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsRetr : case x of
                   125, 150: begin { Do nothing } end;
                   226:
                     begin
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FData.Disconnect(True);  // break on purpose, otherwise we get invalidated ugly
                       Writedbg(['Disconnecting data connection']);
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsStor : case x of
                   125, 150: SendFile;
                   
                   226:
                     begin
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsCWD  : case x of
                   200, 250:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsPWD  : case x of
                   257:
                     begin
                       ParsePWD(Ans);
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsHelp  : case x of
                   211, 214:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;

        fsList : case x of
                   125, 150: begin { do nothing } end;
                   226:
                     begin
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
                 
        fsMKD  : case x of
                   250, 257:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
                 
        fsRMD,
        fsDEL  : case x of
                   250:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FStatusFlags[FStatus.First.Status] := False;
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
                 
        fsRNFR : case x of
                   350:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
                 
        fsRNTO : case x of
                   250:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
        fsFeat : case x of
                   200..299:
                     begin
                       FStatusFlags[FStatus.First.Status] := True;
                       EvaluateFeatures;
                       Eventize(FStatus.First.Status, True);
                     end;
                   else
                     begin
                       FFeatureString := '';
                       Eventize(FStatus.First.Status, False);
                     end;
                 end;
      end;
    end;
  if FStatus.Empty and not FCommandFront.Empty then
    ExecuteFrontCommand;
end;

procedure TLFTPClient.PasvPort;

  function StringPair(const aPort: Word): string;
  begin
    Result := IntToStr(aPort div 256);
    Result := Result + ',' + IntToStr(aPort mod 256);
  end;
  
  function StringIP: string;
  begin
    Result := StringReplace(FControl.Connection.Iterator.LocalAddress, '.', ',',
                          [rfReplaceAll]) + ',';
  end;
  
begin
  if FTransferMethod = ftActive then begin
    Writedbg(['Sent PORT']);
    FData.Disconnect(True);
    FData.Listen(FLastPort);
    FStatus.Insert(MakeStatusRec(fsPort, '', ''));
    FControl.SendMessage('PORT ' + StringIP + StringPair(FLastPort) + FLE);

    if FLastPort < 65535 then
      Inc(FLastPort)
    else
      FLastPort := FStartPort;
  end else begin
    Writedbg(['Sent PASV']);
    FStatus.Insert(MakeStatusRec(fsPasv, '', ''));
    FControl.SendMessage('PASV' + FLE);
  end;
end;

function TLFTPClient.User(const aUserName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsUser, aUserName, '') then begin
    FStatus.Insert(MakeStatusRec(fsUser, '', ''));
    FControl.SendMessage('USER ' + aUserName + FLE);
    Result := True;
  end;
end;

function TLFTPClient.Password(const aPassword: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsPass, aPassword, '') then begin
    FStatus.Insert(MakeStatusRec(fsPass, '', ''));
    FControl.SendMessage('PASS ' + aPassword + FLE);
    Result := True;
  end;
end;

procedure TLFTPClient.SendChunk(const Event: Boolean);
var
  Buf: array[0..65535] of Byte;
  n: Integer;
  Sent: Integer;
begin
  repeat
    n := FStoreFile.Read(Buf, FChunkSize);
    if n > 0 then begin
      Sent := FData.Send(Buf, n);
      if Event and Assigned(FOnSent) and (Sent > 0) then
        FOnSent(FData.Iterator, Sent);
      if Sent < n then
        FStoreFile.Position := FStoreFile.Position - (n - Sent); // so it's tried next time
    end else begin
      if Assigned(FOnSent) then
        FOnSent(FData.Iterator, 0);
      FreeAndNil(FStoreFile);
      FSending := False;
      {$hint this one calls freeinstance which doesn't pass}
      FData.Disconnect(False);
    end;
  until (n = 0) or (Sent = 0);
end;

procedure TLFTPClient.ExecuteFrontCommand;
begin
  with FCommandFront.First do
    case Status of
      fsNone : Exit;
      fsUser : User(Args[1]);
      fsPass : Password(Args[1]);
      fsList : List(Args[1]);
      fsRetr : Retrieve(Args[1]);
      fsStor : Put(Args[1]);
      fsCWD  : ChangeDirectory(Args[1]);
      fsMKD  : MakeDirectory(Args[1]);
      fsRMD  : RemoveDirectory(Args[1]);
      fsDEL  : DeleteFile(Args[1]);
      fsRNFR : Rename(Args[1], Args[2]);
      fsSYS  : SystemInfo;
      fsPWD  : PresentWorkingDirectory;
      fsHelp : Help(Args[1]);
      fsType : SetBinary(StrToBool(Args[1]));
      fsFeat : ListFeatures;
    end;
  FCommandFront.Remove;
end;

function TLFTPClient.Get(out aData; const aSize: Integer; aSocket: TLSocket): Integer;
var
  s: string;
begin
  Result := 0;

  if FControl.Get(aData, aSize, aSocket) > 0 then begin
    SetLength(s, Result);
    Move(aData, PChar(s)^, Result);
    Result := CleanInput(s);
    Move(s[1], aData, Min(Length(s), aSize));
  end;
end;

function TLFTPClient.GetMessage(out msg: string; aSocket: TLSocket): Integer;
begin
  Result := FControl.GetMessage(msg, aSocket);
  if Result > 0 then
    Result := CleanInput(msg);
end;

function TLFTPClient.Send(const aData; const aSize: Integer; aSocket: TLSocket
  ): Integer;
begin
  Result := FControl.Send(aData, aSize);
end;

function TLFTPClient.SendMessage(const msg: string; aSocket: TLSocket
  ): Integer;
begin
  Result := FControl.SendMessage(msg);
end;

function TLFTPClient.GetData(out aData; const aSize: Integer): Integer;
begin
  Result := FData.Iterator.Get(aData, aSize);
end;

function TLFTPClient.GetDataMessage: string;
begin
  Result := '';
  if Assigned(FData.Iterator) then
    FData.Iterator.GetMessage(Result);
end;

function TLFTPClient.Connect(const aHost: string; const aPort: Word): Boolean;
begin
  Result := False;
  Disconnect(True);
  if FControl.Connect(aHost, aPort) then begin
    FHost := aHost;
    FPort := aPort;
    FStatus.Insert(MakeStatusRec(fsCon, '', ''));
    Result := True;
  end;
  if FData.Eventer <> FControl.Connection.Eventer then
    FData.Eventer := FControl.Connection.Eventer;
end;

function TLFTPClient.Connect: Boolean;
begin
  Result := Connect(FHost, FPort);
end;

function TLFTPClient.Authenticate(const aUsername, aPassword: string): Boolean;
begin
  FPassword := aPassWord;
  Result := User(aUserName);
end;

function TLFTPClient.Retrieve(const FileName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsRetr, FileName, '') then begin
    PasvPort;
    FStatus.Insert(MakeStatusRec(fsRetr, '', ''));
    FControl.SendMessage('RETR ' + FileName + FLE);
    Result := True;
  end;
end;

function TLFTPClient.Put(const FileName: string): Boolean;
begin
  Result := not FPipeLine;
  if FileExists(FileName) and CanContinue(fsStor, FileName, '') then begin
    FStoreFile := TFileStream.Create(FileName, fmOpenRead);
    PasvPort;
    FStatus.Insert(MakeStatusRec(fsStor, '', ''));
    FControl.SendMessage('STOR ' + ExtractFileName(FileName) + FLE);
    Result := True;
  end;
end;

function TLFTPClient.ChangeDirectory(const DestPath: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsCWD, DestPath, '') then begin
    FStatus.Insert(MakeStatusRec(fsCWD, '', ''));
    FStatusFlags[fsCWD] := False;
    FControl.SendMessage('CWD ' + DestPath + FLE);
    Result := True;
  end;
end;

function TLFTPClient.MakeDirectory(const DirName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsMKD, DirName, '') then begin
    FStatus.Insert(MakeStatusRec(fsMKD, '', ''));
    FStatusFlags[fsMKD] := False;
    FControl.SendMessage('MKD ' + DirName + FLE);
    Result := True;
  end;
end;

function TLFTPClient.RemoveDirectory(const DirName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsRMD, DirName, '') then begin
    FStatus.Insert(MakeStatusRec(fsRMD, '', ''));
    FStatusFlags[fsRMD] := False;
    FControl.SendMessage('RMD ' + DirName + FLE);
    Result := True;
  end;
end;

function TLFTPClient.DeleteFile(const FileName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsDEL, FileName, '') then begin
    FStatus.Insert(MakeStatusRec(fsDEL, '', ''));
    FStatusFlags[fsDEL] := False;
    FControl.SendMessage('DELE ' + FileName + FLE);
    Result := True;
  end;
end;

function TLFTPClient.Rename(const FromName, ToName: string): Boolean;
begin
  Result := not FPipeLine;
  if CanContinue(fsRNFR, FromName, ToName) then begin
    FStatus.Insert(MakeStatusRec(fsRNFR, '', ''));
    FStatusFlags[fsRNFR] := False;
    FControl.SendMessage('RNFR ' + FromName + FLE);

    FStatus.Insert(MakeStatusRec(fsRNTO, '', ''));
    FStatusFlags[fsRNTO] := False;
    FControl.SendMessage('RNTO ' + ToName + FLE);

    Result := True;
  end;
end;

procedure TLFTPClient.List(const FileName: string = '');
begin
  if CanContinue(fsList, FileName, '') then begin
    PasvPort;
    FStatus.Insert(MakeStatusRec(fsList, '', ''));
    if Length(FileName) > 0 then
      FControl.SendMessage('LIST ' + FileName + FLE)
    else
      FControl.SendMessage('LIST' + FLE);
  end;
end;

procedure TLFTPClient.Nlst(const FileName: string);
begin
  if CanContinue(fsList, FileName, '') then begin
    PasvPort;
    FStatus.Insert(MakeStatusRec(fsList, '', ''));
    if Length(FileName) > 0 then
      FControl.SendMessage('NLST ' + FileName + FLE)
    else
      FControl.SendMessage('NLST' + FLE);
  end;
end;

procedure TLFTPClient.SystemInfo;
begin
  if CanContinue(fsSYS, '', '') then
    FControl.SendMessage('SYST' + FLE);
end;

procedure TLFTPClient.ListFeatures;
begin
  if CanContinue(fsFeat, '', '') then begin
    FStatus.Insert(MakeStatusRec(fsFeat, '', ''));
    FControl.SendMessage('FEAT' + FLE);
  end;
end;

procedure TLFTPClient.PresentWorkingDirectory;
begin
  if CanContinue(fsPWD, '', '') then begin
    FStatus.Insert(MakeStatusRec(fsPWD, '', ''));
    FControl.SendMessage('PWD' + FLE);
  end;
end;

procedure TLFTPClient.Help(const Arg: string);
begin
  if CanContinue(fsHelp, Arg, '') then begin
    FStatus.Insert(MakeStatusRec(fsHelp, Arg, ''));
    FControl.SendMessage('HELP ' + Arg + FLE);
  end;
end;

procedure TLFTPClient.Disconnect(const Forced: Boolean = True);
begin
  FControl.Disconnect(Forced);
  FStatus.Clear;
  FData.Disconnect(Forced);
  FLastPort := FStartPort;
  ClearStatusFlags;
  FCommandFront.Clear;
end;

procedure TLFTPClient.CallAction;
begin
  TLFTPTelnetClient(FControl).CallAction;
end;

initialization
  Randomize;

end.

