{==============================================================================|
| Project : Ararat Synapse                                       | 001.001.001 |
|==============================================================================|
| Content: Trivial FTP (TFTP) client and server                                |
|==============================================================================|
| Copyright (c)1999-2010, Lukas Gebauer                                        |
| All rights reserved.                                                         |
|                                                                              |
| Redistribution and use in source and binary forms, with or without           |
| modification, are permitted provided that the following conditions are met:  |
|                                                                              |
| Redistributions of source code must retain the above copyright notice, this  |
| list of conditions and the following disclaimer.                             |
|                                                                              |
| Redistributions in binary form must reproduce the above copyright notice,    |
| this list of conditions and the following disclaimer in the documentation    |
| and/or other materials provided with the distribution.                       |
|                                                                              |
| Neither the name of Lukas Gebauer nor the names of its contributors may      |
| be used to endorse or promote products derived from this software without    |
| specific prior written permission.                                           |
|                                                                              |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"  |
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE    |
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE   |
| ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR  |
| ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL       |
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR   |
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER   |
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT           |
| LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY    |
| OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH  |
| DAMAGE.                                                                      |
|==============================================================================|
| The Initial Developer of the Original Code is Lukas Gebauer (Czech Republic).|
| Portions created by Lukas Gebauer are Copyright (c)2003-2010.                |
| All Rights Reserved.                                                         |
|==============================================================================|
| Contributor(s):                                                              |
|==============================================================================|
| History: see HISTORY.HTM from distribution package                           |
|          (Found at URL: http://www.ararat.cz/synapse/)                       |
|==============================================================================}

{: @abstract(TFTP client and server protocol)

Used RFC: RFC-1350
}

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}
{$Q-}
{$H+}

{$IFDEF UNICODE}
  {$WARN IMPLICIT_STRING_CAST OFF}
  {$WARN IMPLICIT_STRING_CAST_LOSS OFF}
{$ENDIF}

unit ftptsend;

interface

uses
  SysUtils, Classes,
  blcksock, synautil;

const
  cTFTPProtocol = '69';

  cTFTP_RRQ = word(1);
  cTFTP_WRQ = word(2);
  cTFTP_DTA = word(3);
  cTFTP_ACK = word(4);
  cTFTP_ERR = word(5);

type
  {:@abstract(Implementation of TFTP client and server)
   Note: Are you missing properties for specify server address and port? Look to
   parent @link(TSynaClient) too!}
  TTFTPSend = class(TSynaClient)
  private
    FSock: TUDPBlockSocket;
    FErrorCode: integer;
    FErrorString: string;
    FData: TMemoryStream;
    FRequestIP: string;
    FRequestPort: string;
    function SendPacket(Cmd: word; Serial: word; const Value: string): Boolean;
    function RecvPacket(Serial: word; var Value: string): Boolean;
  public
    constructor Create;
    destructor Destroy; override;

    {:Upload @link(data) as file to TFTP server.}
    function SendFile(const Filename: string): Boolean;

    {:Download file from TFTP server to @link(data).}
    function RecvFile(const Filename: string): Boolean;

    {:Acts as TFTP server and wait for client request. When some request
     incoming within Timeout, result is @true and parametres is filled with
     information from request. You must handle this request, validate it, and
     call @link(ReplyError), @link(ReplyRecv) or @link(ReplySend) for send reply
     to TFTP Client.}
    function WaitForRequest(var Req: word; var filename: string): Boolean;

    {:send error to TFTP client, when you acts as TFTP server.}
    procedure ReplyError(Error: word; Description: string);

    {:Accept uploaded file from TFTP client to @link(data), when you acts as
     TFTP server.}
    function ReplyRecv: Boolean;

    {:Accept download request file from TFTP client and send content of
     @link(data), when you acts as TFTP server.}
    function ReplySend: Boolean;
  published
    {:Code of TFTP error.}
    property ErrorCode: integer read FErrorCode;

    {:Human readable decription of TFTP error. (if is sended by remote side)}
    property ErrorString: string read FErrorString;

    {:MemoryStream with datas for sending or receiving}
    property Data: TMemoryStream read FData;

    {:Address of TFTP remote side.}
    property RequestIP: string read FRequestIP write FRequestIP;

    {:Port of TFTP remote side.}
    property RequestPort: string read FRequestPort write FRequestPort;
  end;

implementation

constructor TTFTPSend.Create;
begin
  inherited Create;
  FSock := TUDPBlockSocket.Create;
  FSock.Owner := self;
  FTargetPort := cTFTPProtocol;
  FData := TMemoryStream.Create;
  FErrorCode := 0;
  FErrorString := '';
end;

destructor TTFTPSend.Destroy;
begin
  FSock.Free;
  FData.Free;
  inherited Destroy;
end;

function TTFTPSend.SendPacket(Cmd: word; Serial: word; const Value: string): Boolean;
var
  s, sh: string;
begin
  FErrorCode := 0;
  FErrorString := '';
  Result := false;
  if Cmd <> 2 then
    s := CodeInt(Cmd) + CodeInt(Serial) + Value
  else
    s := CodeInt(Cmd) + Value;
  FSock.SendString(s);
  s := FSock.RecvPacket(FTimeout);
  if FSock.LastError = 0 then
    if length(s) >= 4 then
    begin
      sh := CodeInt(4) + CodeInt(Serial);
      if Pos(sh, s) = 1 then
        Result := True
      else
        if s[1] = #5 then
        begin
          FErrorCode := DecodeInt(s, 3);
          Delete(s, 1, 4);
          FErrorString := SeparateLeft(s, #0);
        end;
    end;
end;

function TTFTPSend.RecvPacket(Serial: word; var Value: string): Boolean;
var
  s: string;
  ser: word;
begin
  FErrorCode := 0;
  FErrorString := '';
  Result := False;
  Value := '';
  s := FSock.RecvPacket(FTimeout);
  if FSock.LastError = 0 then
    if length(s) >= 4 then
      if DecodeInt(s, 1) = 3 then
      begin
        ser := DecodeInt(s, 3);
        if ser = Serial then
        begin
          Delete(s, 1, 4);
          Value := s;
          S := CodeInt(4) + CodeInt(ser);
          FSock.SendString(s);
          Result := FSock.LastError = 0;
        end
        else
        begin
          S := CodeInt(5) + CodeInt(5) + 'Unexcepted serial#' + #0;
          FSock.SendString(s);
        end;
      end;
      if DecodeInt(s, 1) = 5 then
      begin
        FErrorCode := DecodeInt(s, 3);
        Delete(s, 1, 4);
        FErrorString := SeparateLeft(s, #0);
      end;
end;

function TTFTPSend.SendFile(const Filename: string): Boolean;
var
  s: string;
  ser: word;
  n, n1, n2: integer;
begin
  Result := False;
  FErrorCode := 0;
  FErrorString := '';
  FSock.CloseSocket;
  FSock.Connect(FTargetHost, FTargetPort);
  try
    if FSock.LastError = 0 then
    begin
      s := Filename + #0 + 'octet' + #0;
      if not Sendpacket(2, 0, s) then
        Exit;
      ser := 1;
      FData.Position := 0;
      n1 := FData.Size div 512;
      n2 := FData.Size mod 512;
      for n := 1 to n1 do
      begin
        s := ReadStrFromStream(FData, 512);
//        SetLength(s, 512);
//        FData.Read(pointer(s)^, 512);
        if not Sendpacket(3, ser, s) then
          Exit;
        inc(ser);
      end;
      s := ReadStrFromStream(FData, n2);
//      SetLength(s, n2);
//      FData.Read(pointer(s)^, n2);
      if not Sendpacket(3, ser, s) then
        Exit;
      Result := True;
    end;
  finally
    FSock.CloseSocket;
  end;
end;

function TTFTPSend.RecvFile(const Filename: string): Boolean;
var
  s: string;
  ser: word;
begin
  Result := False;
  FErrorCode := 0;
  FErrorString := '';
  FSock.CloseSocket;
  FSock.Connect(FTargetHost, FTargetPort);
  try
    if FSock.LastError = 0 then
    begin
      s := CodeInt(1) + Filename + #0 + 'octet' + #0;
      FSock.SendString(s);
      if FSock.LastError <> 0 then
        Exit;
      FData.Clear;
      ser := 1;
      repeat
        if not RecvPacket(ser, s) then
          Exit;
        inc(ser);
        WriteStrToStream(FData, s);
//        FData.Write(pointer(s)^, length(s));
      until length(s) <> 512;
      FData.Position := 0;
      Result := true;
    end;
  finally
    FSock.CloseSocket;
  end;
end;

function TTFTPSend.WaitForRequest(var Req: word; var filename: string): Boolean;
var
  s: string;
begin
  Result := False;
  FErrorCode := 0;
  FErrorString := '';
  FSock.CloseSocket;
  FSock.Bind('0.0.0.0', FTargetPort);
  if FSock.LastError = 0 then
  begin
    s := FSock.RecvPacket(FTimeout);
    if FSock.LastError = 0 then
      if Length(s) >= 4 then
      begin
        FRequestIP := FSock.GetRemoteSinIP;
        FRequestPort := IntToStr(FSock.GetRemoteSinPort);
        Req := DecodeInt(s, 1);
        delete(s, 1, 2);
        filename := Trim(SeparateLeft(s, #0));
        s := SeparateRight(s, #0);
        s := SeparateLeft(s, #0);
        Result := lowercase(trim(s)) = 'octet';
      end;
  end;
end;

procedure TTFTPSend.ReplyError(Error: word; Description: string);
var
  s: string;
begin
  FSock.CloseSocket;
  FSock.Connect(FRequestIP, FRequestPort);
  s := CodeInt(5) + CodeInt(Error) + Description + #0;
  FSock.SendString(s);
  FSock.CloseSocket;
end;

function TTFTPSend.ReplyRecv: Boolean;
var
  s: string;
  ser: integer;
begin
  Result := False;
  FErrorCode := 0;
  FErrorString := '';
  FSock.CloseSocket;
  FSock.Connect(FRequestIP, FRequestPort);
  try
    s := CodeInt(4) + CodeInt(0);
    FSock.SendString(s);
    FData.Clear;
    ser := 1;
    repeat
      if not RecvPacket(ser, s) then
        Exit;
      inc(ser);
      WriteStrToStream(FData, s);
//      FData.Write(pointer(s)^, length(s));
    until length(s) <> 512;
    FData.Position := 0;
    Result := true;
  finally
    FSock.CloseSocket;
  end;
end;

function TTFTPSend.ReplySend: Boolean;
var
  s: string;
  ser: word;
  n, n1, n2: integer;
begin
  Result := False;
  FErrorCode := 0;
  FErrorString := '';
  FSock.CloseSocket;
  FSock.Connect(FRequestIP, FRequestPort);
  try
    ser := 1;
    FData.Position := 0;
    n1 := FData.Size div 512;
    n2 := FData.Size mod 512;
    for n := 1 to n1 do
    begin
      s := ReadStrFromStream(FData, 512);
//      SetLength(s, 512);
//      FData.Read(pointer(s)^, 512);
      if not Sendpacket(3, ser, s) then
        Exit;
      inc(ser);
    end;
    s := ReadStrFromStream(FData, n2);
//    SetLength(s, n2);
//    FData.Read(pointer(s)^, n2);
    if not Sendpacket(3, ser, s) then
      Exit;
    Result := True;
  finally
    FSock.CloseSocket;
  end;
end;

{==============================================================================}

end.
