{==============================================================================|
| Project : Ararat Synapse                                       | 003.000.003 |
|==============================================================================|
| Content: SNTP client                                                         |
|==============================================================================|
| 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)2000-2010.                |
| All Rights Reserved.                                                         |
|==============================================================================|
| Contributor(s):                                                              |
|   Patrick Chevalley                                                          |
|==============================================================================|
| History: see HISTORY.HTM from distribution package                           |
|          (Found at URL: http://www.ararat.cz/synapse/)                       |
|==============================================================================}

{:@abstract( NTP and SNTP client)

Used RFC: RFC-1305, RFC-2030
}

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

unit sntpsend;

interface

uses
  SysUtils,
  synsock, blcksock, synautil;

const
  cNtpProtocol = '123';

type

  {:@abstract(Record containing the NTP packet.)}
  TNtp = packed record
    mode: Byte;
    stratum: Byte;
    poll: Byte;
    Precision: Byte;
    RootDelay: Longint;
    RootDisperson: Longint;
    RefID: Longint;
    Ref1: Longint;
    Ref2: Longint;
    Org1: Longint;
    Org2: Longint;
    Rcv1: Longint;
    Rcv2: Longint;
    Xmit1: Longint;
    Xmit2: Longint;
  end;

  {:@abstract(Implementation of NTP and SNTP client protocol),
   include time synchronisation. It can send NTP or SNTP time queries, or it
   can receive NTP broadcasts too.
   
   Note: Are you missing properties for specify server address and port? Look to
   parent @link(TSynaClient) too!}
  TSNTPSend = class(TSynaClient)
  private
    FNTPReply: TNtp;
    FNTPTime: TDateTime;
    FNTPOffset: double;
    FNTPDelay: double;
    FMaxSyncDiff: double;
    FSyncTime: Boolean;
    FSock: TUDPBlockSocket;
    FBuffer: AnsiString;
    FLi, FVn, Fmode : byte;
    function StrToNTP(const Value: AnsiString): TNtp;
    function NTPtoStr(const Value: Tntp): AnsiString;
    procedure ClearNTP(var Value: Tntp);
  public
    constructor Create;
    destructor Destroy; override;

    {:Decode 128 bit timestamp used in NTP packet to TDateTime type.}
    function DecodeTs(Nsec, Nfrac: Longint): TDateTime;

    {:Decode TDateTime type to 128 bit timestamp used in NTP packet.}
    procedure EncodeTs(dt: TDateTime; var Nsec, Nfrac: Longint);

    {:Send request to @link(TSynaClient.TargetHost) and wait for reply. If all
     is OK, then result is @true and @link(NTPReply) and @link(NTPTime) are
     valid.}
    function GetSNTP: Boolean;

    {:Send request to @link(TSynaClient.TargetHost) and wait for reply. If all
     is OK, then result is @true and @link(NTPReply) and @link(NTPTime) are
     valid. Result time is after all needed corrections.}
    function GetNTP: Boolean;

    {:Wait for broadcast NTP packet. If all OK, result is @true and
     @link(NTPReply) and @link(NTPTime) are valid.}
    function GetBroadcastNTP: Boolean;

    {:Holds last received NTP packet.}
    property NTPReply: TNtp read FNTPReply;
  published
    {:Date and time of remote NTP or SNTP server. (UTC time!!!)}
    property NTPTime: TDateTime read FNTPTime;

    {:Offset between your computer and remote NTP or SNTP server.}
    property NTPOffset: Double read FNTPOffset;

    {:Delay between your computer and remote NTP or SNTP server.}
    property NTPDelay: Double read FNTPDelay;

    {:Define allowed maximum difference between your time and remote time for
     synchronising time. If difference is bigger, your system time is not
     changed!}
    property MaxSyncDiff: double read FMaxSyncDiff write FMaxSyncDiff;

    {:If @true, after successfull getting time is local computer clock
     synchronised to given time.
     For synchronising time you must have proper rights! (Usually Administrator)}
    property SyncTime: Boolean read FSyncTime write FSyncTime;

    {:Socket object used for TCP/IP operation. Good for seting OnStatus hook, etc.}
    property Sock: TUDPBlockSocket read FSock;
  end;

implementation

constructor TSNTPSend.Create;
begin
  inherited Create;
  FSock := TUDPBlockSocket.Create;
  FSock.Owner := self;
  FTimeout := 5000;
  FTargetPort := cNtpProtocol;
  FMaxSyncDiff := 3600;
  FSyncTime := False;
end;

destructor TSNTPSend.Destroy;
begin
  FSock.Free;
  inherited Destroy;
end;

function TSNTPSend.StrToNTP(const Value: AnsiString): TNtp;
begin
  if length(FBuffer) >= SizeOf(Result) then
  begin
    Result.mode := ord(Value[1]);
    Result.stratum := ord(Value[2]);
    Result.poll := ord(Value[3]);
    Result.Precision := ord(Value[4]);
    Result.RootDelay := DecodeLongInt(value, 5);
    Result.RootDisperson := DecodeLongInt(value, 9);
    Result.RefID := DecodeLongInt(value, 13);
    Result.Ref1 := DecodeLongInt(value, 17);
    Result.Ref2 := DecodeLongInt(value, 21);
    Result.Org1 := DecodeLongInt(value, 25);
    Result.Org2 := DecodeLongInt(value, 29);
    Result.Rcv1 := DecodeLongInt(value, 33);
    Result.Rcv2 := DecodeLongInt(value, 37);
    Result.Xmit1 := DecodeLongInt(value, 41);
    Result.Xmit2 := DecodeLongInt(value, 45);
  end;

end;

function TSNTPSend.NTPtoStr(const Value: Tntp): AnsiString;
begin
  SetLength(Result, 4);
  Result[1] := AnsiChar(Value.mode);
  Result[2] := AnsiChar(Value.stratum);
  Result[3] := AnsiChar(Value.poll);
  Result[4] := AnsiChar(Value.precision);
  Result := Result + CodeLongInt(Value.RootDelay);
  Result := Result + CodeLongInt(Value.RootDisperson);
  Result := Result + CodeLongInt(Value.RefID);
  Result := Result + CodeLongInt(Value.Ref1);
  Result := Result + CodeLongInt(Value.Ref2);
  Result := Result + CodeLongInt(Value.Org1);
  Result := Result + CodeLongInt(Value.Org2);
  Result := Result + CodeLongInt(Value.Rcv1);
  Result := Result + CodeLongInt(Value.Rcv2);
  Result := Result + CodeLongInt(Value.Xmit1);
  Result := Result + CodeLongInt(Value.Xmit2);
end;

procedure TSNTPSend.ClearNTP(var Value: Tntp);
begin
  Value.mode := 0;
  Value.stratum := 0;
  Value.poll := 0;
  Value.Precision := 0;
  Value.RootDelay := 0;
  Value.RootDisperson := 0;
  Value.RefID := 0;
  Value.Ref1 := 0;
  Value.Ref2 := 0;
  Value.Org1 := 0;
  Value.Org2 := 0;
  Value.Rcv1 := 0;
  Value.Rcv2 := 0;
  Value.Xmit1 := 0;
  Value.Xmit2 := 0;
end;

function TSNTPSend.DecodeTs(Nsec, Nfrac: Longint): TDateTime;
const
  maxi = 4294967295.0;
var
  d, d1: Double;
begin
  d := Nsec;
  if d < 0 then
    d := maxi + d + 1;
  d1 := Nfrac;
  if d1 < 0 then
    d1 := maxi + d1 + 1;
  d1 := d1 / maxi;
  d1 := Trunc(d1 * 10000) / 10000;
  Result := (d + d1) / 86400;
  Result := Result + 2;
end;

procedure TSNTPSend.EncodeTs(dt: TDateTime; var Nsec, Nfrac: Longint);
const
  maxi = 4294967295.0;
  maxilongint = 2147483647;
var
  d, d1: Double;
begin
  d  := (dt - 2) * 86400;
  d1 := frac(d);
  if d > maxilongint then
     d := d - maxi - 1;
  d  := trunc(d);
  d1 := Trunc(d1 * 10000) / 10000;
  d1 := d1 * maxi;
  if d1 > maxilongint then
     d1 := d1 - maxi - 1;
  Nsec:=trunc(d);
  Nfrac:=trunc(d1);
end;

function TSNTPSend.GetBroadcastNTP: Boolean;
var
  x: Integer;
begin
  Result := False;
  FSock.Bind(FIPInterface, FTargetPort);
  FBuffer := FSock.RecvPacket(FTimeout);
  if FSock.LastError = 0 then
  begin
    x := Length(FBuffer);
    if (FTargetHost = '0.0.0.0') or (FSock.GetRemoteSinIP = FSock.ResolveName(FTargetHost)) then
      if x >= SizeOf(NTPReply) then
      begin
        FNTPReply := StrToNTP(FBuffer);
        FNTPTime := DecodeTs(NTPReply.Xmit1, NTPReply.Xmit2);
        if FSyncTime and ((abs(FNTPTime - GetUTTime) * 86400) <= FMaxSyncDiff) then
          SetUTTime(FNTPTime);
        Result := True;
      end;
  end;
end;

function TSNTPSend.GetSNTP: Boolean;
var
  q: TNtp;
  x: Integer;
begin
  Result := False;
  FSock.CloseSocket;
  FSock.Bind(FIPInterface, cAnyPort);
  FSock.Connect(FTargetHost, FTargetPort);
  ClearNtp(q);
  q.mode := $1B;
  FBuffer := NTPtoStr(q);
  FSock.SendString(FBuffer);
  FBuffer := FSock.RecvPacket(FTimeout);
  if FSock.LastError = 0 then
  begin
    x := Length(FBuffer);
    if x >= SizeOf(NTPReply) then
    begin
      FNTPReply := StrToNTP(FBuffer);
      FNTPTime := DecodeTs(NTPReply.Xmit1, NTPReply.Xmit2);
      if FSyncTime and ((abs(FNTPTime - GetUTTime) * 86400) <= FMaxSyncDiff) then
        SetUTTime(FNTPTime);
      Result := True;
    end;
  end;
end;

function TSNTPSend.GetNTP: Boolean;
var
  q: TNtp;
  x: Integer;
  t1, t2, t3, t4 : TDateTime;
begin
  Result := False;
  FSock.CloseSocket;
  FSock.Bind(FIPInterface, cAnyPort);
  FSock.Connect(FTargetHost, FTargetPort);
  ClearNtp(q);
  q.mode := $1B;
  t1 := GetUTTime;
  EncodeTs(t1, q.org1, q.org2);
  FBuffer := NTPtoStr(q);
  FSock.SendString(FBuffer);
  FBuffer := FSock.RecvPacket(FTimeout);
  if FSock.LastError = 0 then
  begin
    x := Length(FBuffer);
    t4 := GetUTTime;
    if x >= SizeOf(NTPReply) then
    begin
      FNTPReply := StrToNTP(FBuffer);
      FLi := (NTPReply.mode and $C0) shr 6;
      FVn := (NTPReply.mode and $38) shr 3;
      Fmode := NTPReply.mode and $07;
      if (Fli < 3) and (Fmode = 4) and
         (NTPReply.stratum >= 1) and (NTPReply.stratum <= 15) and
         (NTPReply.Rcv1 <> 0) and (NTPReply.Xmit1 <> 0)
         then begin
           t2 := DecodeTs(NTPReply.Rcv1, NTPReply.Rcv2);
           t3 := DecodeTs(NTPReply.Xmit1, NTPReply.Xmit2);
           FNTPDelay := (T4 - T1) - (T2 - T3);
           FNTPTime := t3 + FNTPDelay / 2;
           FNTPOffset := (((T2 - T1) + (T3 - T4)) / 2) * 86400;
           FNTPDelay := FNTPDelay * 86400;
           if FSyncTime and ((abs(FNTPTime - t1) * 86400) <= FMaxSyncDiff) then
             SetUTTime(FNTPTime);
           Result := True;
           end
         else result:=false;
    end;
  end;
end;

end.
