{
  $Project$
  $Workfile$
  $Revision$
  $DateUTC$
  $Id$

  This file is part of the Indy (Internet Direct) project, and is offered
  under the dual-licensing agreement described on the Indy website.
  (http://www.indyproject.org/)

  Copyright:
   (c) 1993-2005, Chad Z. Hower and the Indy Pit Crew. All rights reserved.
}
{
  $Log$
}
{
  Rev 1.7    2/8/05 6:08:04 PM  RLebeau
  Changed CheckOptionAck() to use TextIsSame() instead of SameText()

  Rev 1.6    7/23/04 6:41:50 PM  RLebeau
  TFileStream access right tweak for Put()

  Rev 1.5    2/7/2004 7:25:58 PM  JPMugaas
  Deleted error msg code in error packet.  OOPS!!!

  Rev 1.4    2/7/2004 7:20:16 PM  JPMugaas
  DotNET to go!! and YES - I want fries with that :-).

  Rev 1.3    2004.02.03 5:44:38 PM  czhower
  Name changes

  Rev 1.2    1/21/2004 4:21:04 PM  JPMugaas
  InitComponent

  Rev 1.1    2003.10.12 6:36:46 PM  czhower
  Now compiles.

  Rev 1.0    11/13/2002 08:03:32 AM  JPMugaas
}

unit IdTrivialFTP;

interface

{$i IdCompilerDefines.inc}

uses
  Classes,
  IdAssignedNumbers,
  IdGlobal,
  IdTrivialFTPBase,
  IdUDPClient;

const
  GTransferMode = tfOctet;
  GFRequestedBlockSize = 1500;
  GReceiveTimeout = 4000;

type
  TIdTrivialFTP = class(TIdUDPClient)
  protected
    FMode: TIdTFTPMode;
    FRequestedBlockSize: Integer;
    FPeerPort: TIdPort;
    FPeerIP: String;
    function ModeToStr: string;
    procedure CheckOptionAck(const OptionPacket: TIdBytes; Reading: Boolean);
  protected
    procedure SendAck(const BlockNumber: UInt16);
    procedure RaiseError(const ErrorPacket: TIdBytes);
    procedure InitComponent; override;
  public
    procedure Get(const ServerFile: String; DestinationStream: TStream); overload;
    procedure Get(const ServerFile, LocalFile: String); overload;
    procedure Put(SourceStream: TStream; const ServerFile: String); overload;
    procedure Put(const LocalFile, ServerFile: String); overload;
  published
    property TransferMode: TIdTFTPMode read FMode write FMode Default GTransferMode;
    property RequestedBlockSize: Integer read FRequestedBlockSize write FRequestedBlockSize default 1500;
    property OnWork;
    property OnWorkBegin;
    property OnWorkEnd;
  end;

implementation

uses
  {$IFDEF DOTNET}
  IdStreamNET,
  {$ELSE}
  IdStreamVCL,
  {$ENDIF}
  IdComponent,
  IdExceptionCore,
  IdGlobalProtocols,
  IdResourceStringsProtocols,
  IdStack,
  SysUtils;

procedure TIdTrivialFTP.CheckOptionAck(const OptionPacket: TIdBytes; Reading: Boolean);
var
  LOptName, LOptValue: String;
  LOffset, Idx, OptionIdx: Integer;
  LRequestedBlkSize: Integer;
begin
  LOffset := 2; // skip packet opcode

  try
    while LOffset < Length(OptionPacket) do
    begin
      Idx := ByteIndex(0, OptionPacket, LOffset);
      if Idx = -1 then begin
        raise EIdTFTPOptionNegotiationFailed.Create('');
      end;

      LOptName := BytesToString(OptionPacket, LOffset, Idx-LOffset, IndyTextEncoding_ASCII);
      LOffset := Idx+1;

      Idx := ByteIndex(0, OptionPacket, LOffset);
      if Idx = -1 then begin
        raise EIdTFTPOptionNegotiationFailed.Create('');
      end;

      LOptValue := BytesToString(OptionPacket, LOffset, Idx-LOffset, IndyTextEncoding_ASCII);
      LOffset := Idx+1;

      OptionIdx := PosInStrArray(LOptName, [sBlockSize, sTransferSize], False);
      if OptionIdx = -1 then begin
        // RLebeau 12/6/2011: workaround for bug in PicoMOD3 devices
        if (LOptName = '') and (LOptValue = '') then begin
          Continue;
        end;
        raise EIdTFTPOptionNegotiationFailed.CreateFmt(RSTFTPUnsupportedOption, [LOptName]);
      end;

      case OptionIdx of
        0:
          begin
            LRequestedBlkSize := IndyStrToInt(LOptValue);
            if (LRequestedBlkSize < 8) or (LRequestedBlkSize > 65464) then begin
              raise EIdTFTPOptionNegotiationFailed.CreateFmt(RSTFTPUnsupportedOptionValue, [LOptValue, LOptName]);
            end;
            BufferSize := 4 + LRequestedBlkSize;
          end;
        1:
          begin
            if Reading then
            begin
              // TODO
              {
              if (IndyStrToInt(LOptValue) is not available) then begin
                raise EIdTFTPAllocationExceeded.Create('');
              end;
              }
            end;
          end;
      end;
    end;
  except
    on E: Exception do begin
      SendError(Self, FPeerIP, FPeerPort, E);
      raise;
    end;
  end;
end;

procedure TIdTrivialFTP.InitComponent;
begin
  inherited;
  TransferMode := GTransferMode;
  Port := IdPORT_TFTP;
  FRequestedBlockSize := GFRequestedBlockSize;
  ReceiveTimeout := GReceiveTimeout;
end;

procedure TIdTrivialFTP.Get(const ServerFile: String; DestinationStream: TStream);
var
  Buffer, LServerFile, LMode, LBlockSize, LBlockOctets, LTransferSize, LTransferOctets: TIdBytes;
  DataLen, LOffset: Integer;
  ExpectedBlockCtr, RecvdBlockCtr: UInt16;
  TerminateTransfer: Boolean;
begin
  try
    BufferSize := 4 + 512;   // 512 as specified by RFC 1350

    LServerFile := ToBytes(ServerFile);
    LMode := ToBytes(ModeToStr);
    LBlockSize := ToBytes(sBlockSize);
    LBlockOctets := ToBytes(IntToStr(FRequestedBlockSize));
    LTransferSize := ToBytes(sTransferSize);
    LTransferOctets := ToBytes(IntToStr(0));

    SetLength(Buffer, 2+Length(LServerFile)+1+Length(LMode)+1+Length(LBlockSize)+1+Length(LBlockOctets)+1+Length(LTransferSize)+1+Length(LTransferOctets)+1);
    LOffset := 0;

    CopyTIdUInt16(GStack.HostToNetwork(UInt16(TFTP_RRQ)), Buffer, LOffset);
    Inc(LOffset, 2);
    CopyTIdBytes(LServerFile, 0, Buffer, LOffset, Length(LServerFile));
    Inc(LOffset, Length(LServerFile));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LMode, 0, Buffer, LOffset, Length(LMode));
    Inc(LOffset, Length(LMode));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LBlockSize, 0, Buffer, LOffset, Length(LBlockSize));
    Inc(LOffset, Length(LBlockSize));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LBlockOctets, 0, Buffer, LOffset, Length(LBlockOctets));
    Inc(LOffset, Length(LBlockOctets));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LTransferSize, 0, Buffer, LOffset, Length(LTransferSize));
    Inc(LOffset, Length(LTransferSize));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LTransferOctets, 0, Buffer, LOffset, Length(LTransferOctets));
    Inc(LOffset, Length(LTransferOctets));
    Buffer[LOffset] := 0;

    SendBuffer(Buffer);

    ExpectedBlockCtr := 1;
    TerminateTransfer := False;

    BeginWork(wmRead);
    try
      repeat
        SetLength(Buffer, BufferSize);
        DataLen := ReceiveBuffer(Buffer, FPeerIP, FPeerPort, ReceiveTimeout);
        if DataLen <= 0 then begin
          // TODO: re-transmit the last sent packet again instead of erroring...
          raise EIdTFTPException.Create(RSTimeOut);
        end;
        SetLength(Buffer, DataLen);
        // TODO: validate the correct peer is sending the data...
        case GStack.NetworkToHost(BytesToUInt16(Buffer)) of
          TFTP_DATA:
            begin
              RecvdBlockCtr := GStack.NetworkToHost(BytesToUInt16(Buffer, 2));
              if RecvdBlockCtr = ExpectedBlockCtr then
              begin
                DataLen := Length(Buffer) - 4;
                try
                  WriteTIdBytesToStream(DestinationStream, Buffer, DataLen, 4);
                  DoWork(wmRead, DataLen);
                except
                  on E: Exception do
                  begin
                    SendError(Self, FPeerIP, FPeerPort, E);
                    raise;
                  end;
                end;
                SendAck(RecvdBlockCtr);
                if RecvdBlockCtr = High(UInt16) then begin
                  if Length(Buffer) >= BufferSize then begin
                    // have reached the max block counter allowed, can't validate any more data...
                    SendError(Self, FPeerIP, FPeerPort, ErrIllegalOperation, '');
                    raise EIdTFTPException.CreateFmt(RSTFTPUnexpectedOp, [FPeerIP, FPeerPort]);
                  end;
                  TerminateTransfer := True; // end of transfer, a block counter cannot wrap back to 0
                end else begin
                  ExpectedBlockCtr := RecvdBlockCtr + 1;
                  TerminateTransfer := Length(Buffer) < BufferSize;
                end;
              end;
            end;
          TFTP_ERROR:
            begin
              RaiseError(Buffer);
            end;
          TFTP_OACK:
            begin
              CheckOptionAck(Buffer, True);
              SendAck(0);
            end;
          else
            begin
              SendError(Self, FPeerIP, FPeerPort, ErrIllegalOperation, '');
              raise EIdTFTPException.CreateFmt(RSTFTPUnexpectedOp, [FPeerIP, FPeerPort]);
            end;
        end;
      until TerminateTransfer;
    finally
      EndWork(wmRead);
    end;
  finally
    Binding.CloseSocket;
  end;
end;

procedure TIdTrivialFTP.Get(const ServerFile, LocalFile: String);
var
  fs: TFileStream;
begin
  fs := TIdFileCreateStream.Create(LocalFile);
  try
    Get(ServerFile, fs);
  finally
    FreeAndNil(fs);
  end;
end;

function TIdTrivialFTP.ModeToStr: string;
begin
  case TransferMode of
    tfNetAscii: Result := 'netascii'; {Do not Localize}
    tfOctet:    Result := 'octet';    {Do not Localize}
  end;
end;

procedure TIdTrivialFTP.Put(SourceStream: TStream; const ServerFile: String);
var
  Buffer, LServerFile, LMode, LBlockSize, LBlockOctets, LTransferSize, LTransferOctets: TIdBytes;
  StreamLen: TIdStreamSize;
  LOffset, DataLen: Integer;
  ExpectedBlockCtr, RecvdBlockCtr, wOp: UInt16;
  TerminateTransfer, WaitingForAck: Boolean;

  procedure SendDataPacket(const BlockNumber: UInt16);
  begin
    DataLen := IndyMin(BufferSize-4, StreamLen);
    SetLength(Buffer, 4 + DataLen);
    CopyTIdUInt16(GStack.HostToNetwork(UInt16(TFTP_DATA)), Buffer, 0);
    CopyTIdUInt16(GStack.HostToNetwork(BlockNumber), Buffer, 2);
    try
      DataLen := ReadTIdBytesFromStream(SourceStream, Buffer, DataLen, 4);
    except
      on E: Exception do
      begin
        SendError(Self, FPeerIP, FPeerPort, E);
        raise;
      end;
    end;
    SetLength(Buffer, 4 + DataLen);
    SendBuffer(FPeerIP, FPeerPort, Buffer);
    WaitingForAck := True;
    DoWork(wmWrite, DataLen);
    Dec(StreamLen, DataLen);
    TerminateTransfer := DataLen < (BufferSize - 4);
    ExpectedBlockCtr := BlockNumber;
  end;

begin
  try
    BufferSize := 4 + 512;   // 512 as specified by RFC 1350

    StreamLen := SourceStream.Size - SourceStream.Position;

    LServerFile := ToBytes(ServerFile);
    LMode := ToBytes(ModeToStr);
    LBlockSize := ToBytes(sBlockSize);
    LBlockOctets := ToBytes(IntToStr(FRequestedBlockSize));
    LTransferSize := ToBytes(sTransferSize);
    LTransferOctets := ToBytes(IntToStr(StreamLen));

    SetLength(Buffer, 2+Length(LServerFile)+1+Length(LMode)+1+Length(LBlockSize)+1+Length(LBlockOctets)+1+Length(LTransferSize)+1+Length(LTransferOctets)+1);
    LOffset := 0;

    CopyTIdUInt16(GStack.HostToNetwork(UInt16(TFTP_WRQ)), Buffer, LOffset);
    Inc(LOffset, 2);
    CopyTIdBytes(LServerFile, 0, Buffer, LOffset, Length(LServerFile));
    Inc(LOffset, Length(LServerFile));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LMode, 0, Buffer, LOffset, Length(LMode));
    Inc(LOffset, Length(LMode));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LBlockSize, 0, Buffer, LOffset, Length(LBlockSize));
    Inc(LOffset, Length(LBlockSize));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LBlockOctets, 0, Buffer, LOffset, Length(LBlockOctets));
    Inc(LOffset, Length(LBlockOctets));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LTransferSize, 0, Buffer, LOffset, Length(LTransferSize));
    Inc(LOffset, Length(LTransferSize));
    Buffer[LOffset] := 0;
    Inc(LOffset);
    CopyTIdBytes(LTransferOctets, 0, Buffer, LOffset, Length(LTransferOctets));
    Inc(LOffset, Length(LTransferOctets));
    Buffer[LOffset] := 0;

    SendBuffer(Buffer);

    ExpectedBlockCtr := 0;
    TerminateTransfer := False;

    BeginWork(wmWrite, StreamLen);
    try
      repeat
        SetLength(Buffer, BufferSize);
        DataLen := ReceiveBuffer(Buffer, FPeerIP, FPeerPort, IndyMax(500, ReceiveTimeout));
        if DataLen <= 0 then begin
          // TODO: re-transmit the last sent packet again instead of erroring...
          raise EIdTFTPException.Create(RSTimeOut);
        end;
        SetLength(Buffer, DataLen);
        // TODO: validate the correct peer is sending the data...
        wOp := GStack.NetworkToHost(BytesToUInt16(Buffer));
        case wOp of
          TFTP_ACK:
            begin
              RecvdBlockCtr := GStack.NetworkToHost(BytesToUInt16(Buffer, 2));
              if RecvdBlockCtr = ExpectedBlockCtr then
              begin
                WaitingForAck := False;
                if not TerminateTransfer then
                begin
                  if RecvdBlockCtr = High(UInt16) then
                  begin
                    // end of transfer, a block counter cannot wrap back to 0
                    SendError(Self, FPeerIP, FPeerPort, ErrAllocationExceeded, '');
                    raise EIdTFTPAllocationExceeded.Create('');
                  end;
                  SendDataPacket(RecvdBlockCtr+1);
                end;
              end;
            end;
          TFTP_OACK:
            begin
              if ExpectedBlockCtr <> 0 then
              begin
                SendError(Self, FPeerIP, FPeerPort, ErrIllegalOperation, '');
                raise EIdTFTPException.CreateFmt(RSTFTPUnexpectedOp, [FPeerIP, FPeerPort]);
              end;
              CheckOptionAck(Buffer, False);
              SendDataPacket(1);
            end;
          TFTP_ERROR:
            begin
              RaiseError(Buffer);
            end;
         else
            begin
              SendError(Self, FPeerIP, FPeerPort, ErrIllegalOperation, '');
              raise EIdTFTPException.CreateFmt(RSTFTPUnexpectedOp, [FPeerIP, FPeerPort]);
            end;
        end;
      until TerminateTransfer and (not WaitingForAck);
    finally
      EndWork(wmWrite);
    end;
  finally
    Binding.CloseSocket;
  end;
end;

procedure TIdTrivialFTP.Put(const LocalFile, ServerFile: String);
var
  fs: TIdReadFileExclusiveStream;
begin
  fs := TIdReadFileExclusiveStream.Create(LocalFile);
  try
    Put(fs, ServerFile);
  finally
    fs.Free;
  end;
end;

procedure TIdTrivialFTP.RaiseError(const ErrorPacket: TIdBytes);
var
  ErrMsg: string;
begin
  ErrMsg := BytesToString(ErrorPacket, 4, Length(ErrorPacket)-4, IndyTextEncoding_ASCII);
  case GStack.NetworkToHost(BytesToUInt16(ErrorPacket, 2)) of
    ErrFileNotFound:            raise EIdTFTPFileNotFound.Create(ErrMsg);
    ErrAccessViolation:         raise EIdTFTPAccessViolation.Create(ErrMsg);
    ErrAllocationExceeded:      raise EIdTFTPAllocationExceeded.Create(ErrMsg);
    ErrIllegalOperation:        raise EIdTFTPIllegalOperation.Create(ErrMsg);
    ErrUnknownTransferID:       raise EIdTFTPUnknownTransferID.Create(ErrMsg);
    ErrFileAlreadyExists:       raise EIdTFTPFileAlreadyExists.Create(ErrMsg);
    ErrNoSuchUser:              raise EIdTFTPNoSuchUser.Create(ErrMsg);
    ErrOptionNegotiationFailed: raise EIdTFTPOptionNegotiationFailed.Create(ErrMsg);
  else
    // usually ErrUndefined (see EIdTFTPException.Message if any)
    raise EIdTFTPException.Create(ErrMsg);
  end;
end;

procedure TIdTrivialFTP.SendAck(const BlockNumber: UInt16);
begin
  SendBuffer(FPeerIP, FPeerPort, MakeActPkt(BlockNumber));
end;

end.
