- Pods
- studs 1.0.2
- API
- SntpClient
- Src
sourcestuds::SntpClient.fan
//
// Copyright (c) 2016, Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 10 Sep 2016 Andy Frank Creation
//
using inet
**
** SntpClient retrieves network time using SNTP protocol.
**
class SntpClient
{
**
** Request the time offset between the current system clock and
** the given NTP host. Note that NTP requires the requesting
** host be within 34 years of the server time. Therefore if a
** system has not had time configured, it should be set a date
** close to the present.
**
static Duration offset(IpAddr host, Int port := 123, Duration timeout := 10sec)
{
UdpSocket? socket := null
try
{
socket = UdpSocket()
socket.options.receiveTimeout = timeout
// send request
req := Buf(48)
reqTs := toNtp(DateTime.nowUtc(null))
req.write(3.shiftl(3).or(3)) // ver=3 | mode=3
req.write(0) // stratum
req.write(0) // poll
req.write(0) // precision
req.writeI4(0) // root delay
req.writeI4(0) // root dispersion
req.writeI4(0) // reference id
req.writeI8(0) // reference ts
req.writeI8(0) // orig ts
req.writeI8(0) // recv ts
req.writeI8(reqTs) // transmit time
packet := UdpPacket(host, port, req.flip)
socket.send(packet)
// read response
res := socket.receive.data
destTs := toNtp(DateTime.now(null))
first := res.seek(0).read
leapInd := first.shiftl(6).and(0x03)
mode := first.and(0x07)
stratum := res.read
poll := res.read
prec := res.read
rootDelay := res.readU4
rootDisp := res.readU4
refId := res.readU4
refTs := res.readS8
origTs := res.readS8
rxTs := res.readS8
txTs := res.readS8
// sanity checks per RFC 4330 - Section 5/6
// verify response mode server=4 or broadcast=5
if (mode != 4 && mode != 5) throw Err("Unsupported mode response: $mode")
// server not synchronized
if (stratum == 0) throw Err("Server not synchronized: ${toRefCode(refId)}")
if (leapInd == 3) throw Err("Server not synchronized")
if (refTs == 0 || origTs == 0 || rxTs == 0) throw Err("Server not synchronized")
// verify bounds
if (stratum > 15) throw Err("Unsupported stratum: $stratum")
if (reqTs != origTs) throw Err("Request timestamp ($reqTs) != Originate timestamp ($origTs)")
delay := (destTs - origTs) - (txTs - rxTs)
offset := ((rxTs - origTs) + (txTs - destTs)) / 2
return Duration(offset)
}
finally { socket?.close }
}
** Get reference code from reference id.
private static Str toRefCode(Int refId)
{
a := refId.shiftr(24).and(0x0ff)
b := refId.shiftr(16).and(0x0ff)
c := refId.shiftr(8).and(0x0ff)
d := refId.and(0x0ff)
buf := StrBuf()
if (a != 0) buf.addChar(a)
if (b != 0) buf.addChar(b)
if (c != 0) buf.addChar(c)
if (d != 0) buf.addChar(d)
return buf.toStr
}
**
** Convert a DateTime instance to NTP timestamp, where time is
** represented with a 64-bit field as seconds since midnight
** Jan 1, 1900:
**
** Bits 00-31: seconds
** Bits 32-63: seconds fraction (in picoseconds)
**
@NoDoc static Int toNtp(DateTime ts)
{
ns := ts.ticks
sec := ns / nsInSec
frac := (ns % nsInSec) * 0x100_000_000 / nsInSec
return (sec + ntpEpoch).shiftl(32).or(frac)
}
**
** Convert NTP timestamp back to a DateTime instance.
** See `toNtp` for conversion notes.
**
@NoDoc static DateTime fromNtp(Int ntp, TimeZone tz := TimeZone.cur)
{
sec := ntp.shiftr(32).and(0xffff_ffff) - ntpEpoch
frac := ntp.and(0xffff_ffff) * nsInSec / 0x100_000_000
ns := (sec * nsInSec) + frac
return DateTime.makeTicks(ns, tz)
}
// secs between NTP/Fan epochs 1/1/1900..1/1/20000
private static const Int ntpEpoch := 36524 * 1day.toSec
// ns in 1sec
private static const Int nsInSec := 1_000_000_000
}