Skip to content

Instantly share code, notes, and snippets.

@MircoBabin
Created October 16, 2022 18:04
Show Gist options
  • Save MircoBabin/e40b3351e7d30c3dbc0a297f68479de7 to your computer and use it in GitHub Desktop.
Save MircoBabin/e40b3351e7d30c3dbc0a297f68479de7 to your computer and use it in GitHub Desktop.
PHP ntp client for retrieving time via Network Time Protocol (ntp, sntp, RFC 1769, RFC 4330) - Php 5.4.44 and later - sunday 16 october 2022
<?php
/*
MIT license
Copyright (c) 2022 Mirco Babin
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
/*
Filename : NetworkTimeProtocolClient.php
Version : 1.0
Published: sunday 16 october 2022
---------------------------------
//Example 1: Get time from ntp server.
require_once('NetworkTimeProtocolClient.php');
try {
$ntp = new \NetworkTimeProtocol\Client();
$ntpTime = $ntp->getTime();
echo date('Y-m-d H:i:s', $ntpEqual['ntpTime']).' reported by '.$ntp->getHost().' ('.$ntpEqual['ntpTime'].')'.PHP_EOL;
} catch (\Exception $ex) {
echo 'NTP error. '.$ex->getMessage().PHP_EOL;
}
//Example 2: Check if ntp server time equals system time within a margin. Assuming the ntp server has the right time and the system time maybe off.
require_once('NetworkTimeProtocolClient.php');
try {
$ntp = new \NetworkTimeProtocol\Client();
$ntpEqual = $ntp->equalsSystemTime(600); // 10 minutes margin
if (!$ntpEqual['equalWithinMargin']) {
echo 'Error: system time is not correct compared to ntp time with a margin of 10 minutes.'.PHP_EOL;
echo date('Y-m-d H:i:s', $ntpEqual['time']).' is the system time'.' ('.$ntpEqual['time'].')'.PHP_EOL;
echo date('Y-m-d H:i:s', $ntpEqual['ntpTime']).' reported by '.$ntp->getHost().' ('.$ntpEqual['ntpTime'].')'.PHP_EOL;
return;
}
} catch (\Exception $ex) {
echo 'NTP error. '.$ex->getMessage().PHP_EOL;
}
*/
namespace NetworkTimeProtocol;
class Client
{
private $host;
private $port;
private $connectTimeoutInSeconds;
private $communicateTimeoutInSeconds;
private $acceptKissOfDeathMessage;
private $kissOfDeathMessageReceived;
/*
Each key in the $options array is optional. When omitted the default value listed will be used.
$options = array(
'host' => 'pool.ntp.org',
'port' => 123, // 123 is the default udp port used for ntp.
'connectTimeoutInSeconds' => 15, // timeout for establishing connection to 'host' on 'port'.
'communicateTimeoutInSeconds' => 5, // timeout for communication: sending ntp request packet (48 bytes) and receiving ntp response packet (48 bytes) via udp.
'acceptKissOfDeathMessage' => true, // when kiss-of-death message is received from this ntp server, stop all communication to this ntp server.
);
*/
public function __construct($options = null)
{
$this->host = 'pool.ntp.org';
$this->port = 123;
$this->connectTimeoutInSeconds = 15;
$this->communicateTimeoutInSeconds = 5;
$this->acceptKissOfDeathMessage = true;
$this->kissOfDeathMessageReceived = false;
if (is_array($options)) {
if (array_key_exists('host', $options)) $this->host = strval($options['host']);
if (array_key_exists('port', $options)) $this->port = intval($options['port']);
if (array_key_exists('connectTimeoutInSeconds', $options)) $this->connectTimeoutInSeconds = intval($options['connectTimeoutInSeconds']);
if (array_key_exists('communicateTimeoutInSeconds', $options)) $this->communicateTimeoutInSeconds = intval($options['communicateTimeoutInSeconds']);
if (array_key_exists('acceptKissOfDeathMessage', $options)) $this->acceptKissOfDeathMessage = ($options['acceptKissOfDeathMessage'] !== false);
}
}
public function getHost()
{
return $this->host;
}
public function getPort()
{
return $this->port;
}
public function getConnectTimeoutInSeconds()
{
return $this->connectTimeoutInSeconds;
}
public function getCommunicateTimeoutInSeconds()
{
return $this->communicateTimeoutInSeconds;
}
public function getAcceptKissOfDeathMessage()
{
return $this->acceptKissOfDeathMessage;
}
public function isKissOfDeatchMessageReceived()
{
return $this->kissOfDeathMessageReceived;
}
public function equalsSystemTime($marginInSeconds)
{
$ntpTime = $this->getTime();
$time = time();
return array(
'equalWithinMargin' => ($ntpTime >= ($time - $marginInSeconds) && $ntpTime <= ($time + $marginInSeconds)),
'ntpTime' => $ntpTime,
'time' => $time,
);
}
private function checkCommunicateTimeout($timeoutStartTime, $ntpName)
{
$timeoutEndTime = time();
if ($timeoutEndTime < $timeoutStartTime)
throw ('Timed out for '.$ntpName.'. System clock overflow detected, start timestamp was '.$timeoutStartTime.', current timestamp is '.$timeoutEndTime.'.');
$timeout = $this->communicateTimeoutInSeconds - ($timeoutEndTime - $timeoutStartTime);
if ($timeout <= 0)
throw new \Exception('Timed out for '.$ntpName.'.');
return $timeout;
}
public function getTime()
{
$ntpName = 'udp://'.$this->host.' on port '.$this->port;
if ($this->kissOfDeathMessageReceived)
throw new \Exception('A kiss-of-death message was received from '.$ntpName.'. Stop all communication to this server.');
if ($this->connectTimeoutInSeconds <= 0)
throw new \Exception('Error opening '.$ntpName.'. Connect timeout of '.$this->connectTimeoutInSeconds.' seconds is 0 or negative.');
if ($this->communicateTimeoutInSeconds <= 0)
throw new \Exception('Error opening '.$ntpName.'. Communicate timeout of '.$this->communicateTimeoutInSeconds.' seconds is 0 or negative.');
{
$errno = 0;
$errmsg = '';
$stream = @fsockopen(
'udp://'.$this->host,
$this->port,
$errno,
$errmsg,
$this->connectTimeoutInSeconds /* seconds timeout */
);
if ($stream === false)
throw new \Exception('Error opening '.$ntpName.'. '.$errno.': '.$errmsg);
}
$timeout = $this->communicateTimeoutInSeconds;
$timeoutStartTime = time();
{
$selectRead = null;
$selectWrite = array($stream);
$selectExcept = null;
$result = stream_select($selectRead, $selectWrite, $selectExcept, $timeout /* seconds timeout */);
if ($result === false || $result != 1)
throw new \Exception('Timed out waiting for send packet to '.$ntpName.'.');
}
$timeout = $this->checkCommunicateTimeout($timeoutStartTime, $ntpName);
{
$packet = chr(0x1b).str_repeat("\0", 47); // LeapIndicator = 0, VersionNumber = 3, Mode = 3
$result = @fwrite($stream, $packet);
if ($result === false || $result != 48)
throw new \Exception('Error sending packet to '.$ntpName.'.');
@fflush($stream);
}
$timeout = $this->checkCommunicateTimeout($timeoutStartTime, $ntpName);
{
$selectRead = array($stream);
$selectWrite = null;
$selectExcept = null;
$result = stream_select($selectRead, $selectWrite, $selectExcept, $timeout /* seconds timeout */);
if ($result === false || $result != 1)
throw new \Exception('Timed out waiting for packet from '.$ntpName.'.');
}
$timeout = $this->checkCommunicateTimeout($timeoutStartTime, $ntpName);
{
if (stream_set_timeout($stream, $timeout) === false)
throw new \Exception('Error setting read timeout for receiving packet from '.$ntpName.'.');
$packet = @fread($stream, 48);
if ($packet === false || strlen($packet) != 48)
throw new \Exception('Error receiving packet from '.$ntpName.'.');
}
fclose($stream);
$ntpPacket = $this->unpackNtpPacket($packet);
//echo $this->dumpUnpackedNtpPacket($ntpPacket, $ntpName);
switch($ntpPacket['Mode'])
{
case 4: //server (unicast)
case 5: //broadcast
break;
default:
throw new \Exception('Received Mode must be 4 (unicast) or 5 (broadcast), but is '.$ntpPacket['Mode'].'.');
}
if ($ntpPacket['LeapIndicator'] == 3)
throw new \Exception('Received Leap Indicator (LI) is 3 (alarm condition - clock not synchronized).');
if ($ntpPacket['VersionNumber'] != 3)
throw new \Exception('Received Version Number (VN) must be 3 (RFC 1769 - SNMP Version 3 - march 1995), but is '.$ntpPacket['VersionNumber'].'.');
if ($ntpPacket['Stratum'] == 0) {
if ($this->acceptKissOfDeathMessage)
$this->kissOfDeathMessageReceived = true;
throw new \Exception('Received Stratum is 0 (unspecified or unavailable / kiss-of-death RFC 4330 - SNMP version 4).');
}
if ($ntpPacket['Stratum'] < 1 || $ntpPacket['Stratum'] > 15)
throw new \Exception('Received Stratum must be 1 .. 15, but is '.$ntpPacket['Stratum'].'.');
if ($ntpPacket['TransmitTimestamp']['NtpSeconds'] == 0 && $ntpPacket['TransmitTimestamp']['NtpSecondsFraction'] == 0)
throw new \Exception('Received Transmit Timestamp must not be 0.');
return $ntpPacket['TransmitTimestamp']['PhpTimestamp'];
}
private function unpackNtpPacket($packet)
{
/* RFC 1769 - SNMP version 3 - march 1995
NTP Packet, Big-Endian, bit 0 is high bit.
1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference Identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Reference Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Originate Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Receive Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Transmit Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Key Identifier (optional) (32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| |
| Message Digest (optional) (128) |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
if (strlen($packet) < 48)
throw new \Exception('NTP packet should be at least 48 bytes.');
$byte0 = ord(substr($packet, 0, 1));
$unpacked = unpack('c', substr($packet, 3, 1)); //signed
$precision = reset($unpacked);
$unpacked = unpack('n', substr($packet, 4, 4)); //signed
$rootDelay = reset($unpacked);
$unpacked = unpack('N', substr($packet, 8, 4)); //unsigned
$rootDispersion = reset($unpacked);
return array(
'LeapIndicator' => (($byte0 & 0xC0) >> 6),
'VersionNumber' => (($byte0 & 0x38) >> 3),
'Mode' => ($byte0 & 0x07),
'Stratum' => ord(substr($packet, 1, 1)), //unsigned
'PollInterval' => ord(substr($packet, 2, 1)), //unsigned
'Precision' => $precision, //signed
'RootDelay' => $rootDelay, //signed
'RootDispersion' => $rootDispersion, //unsigned
'ReferenceIdentifier' => substr($packet, 12, 4), //4 bytes
'ReferenceTimestamp' => $this->unpackNtpTimestamp(substr($packet, 16, 8)),
'OriginateTimestamp' => $this->unpackNtpTimestamp(substr($packet, 24, 8)),
'ReceiveTimestamp' => $this->unpackNtpTimestamp(substr($packet, 32, 8)),
'TransmitTimestamp' => $this->unpackNtpTimestamp(substr($packet, 40, 8)),
);
}
private function unpackNtpTimestamp($ntpTimestamp)
{
/* RFC 1769 - SNMP version 3 - march 1995
NTP timestamp, Big-Endian, bit 0 is high bit.
1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seconds |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Seconds Fraction (0-padded) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Note that since some time in 1968 (second 2,147,483,648), the most
significant bit (bit 0 of the integer part) has been set and that the
64-bit field will overflow some time in 2036 (second 4,294,967,296).
There will exist a 232-picosecond interval, henceforth ignored, every
136 years when the 64-bit field will be 0, which by convention is
interpreted as an invalid or unavailable timestamp.
As the NTP timestamp format has been in use for over 20 years, it
is possible that it will be in use 32 years from now, when the
seconds field overflows. As it is probably inappropriate to
archive NTP timestamps before bit 0 was set in 1968, a convenient
way to extend the useful life of NTP timestamps is the following
convention: If bit 0 is set, the UTC time is in the range 1968-
2036, and UTC time is reckoned from 0h 0m 0s UTC on 1 January
1900. If bit 0 is not set, the time is in the range 2036-2104 and
UTC time is reckoned from 6h 28m 16s UTC on 7 February 2036. Note
that when calculating the correspondence, 2000 is a leap year, and
leap seconds are not included in the reckoning.
The arithmetic calculations used by NTP to determine the clock
offset and roundtrip delay require the client time to be within 34
years of the server time before the client is launched. As the
time since the Unix base 1970 is now more than 34 years, means
must be available to initialize the clock at a date closer to the
present, either with a time-of-year (TOY) chip or from firmware.
*/
if (strlen($ntpTimestamp) !== 8)
throw new \Exception('NTP timestamp should be 8 bytes.');
$unpacked = unpack('N', substr($ntpTimestamp, 0, 4));
$seconds = reset($unpacked);
$unpacked = unpack('N', substr($ntpTimestamp, 4, 4));
$secondsFraction = reset($unpacked);
return array(
'NtpSeconds' => $seconds,
'NtpSecondsFraction' => $secondsFraction,
'PhpTimestamp' => strtotime(date('Y-m-d H:i:s', $seconds - 2208988800)), // subtract 70 years in seconds AND make sure no negative number is returned.
);
}
private function dumpUnpackedNtpTimestamp($ntpTimestamp, $prefixLine = '', $endOfLine = PHP_EOL)
{
$output = '';
$output .= $prefixLine.'Seconds : '.$ntpTimestamp['NtpSeconds'].$endOfLine;
$output .= $prefixLine.'Seconds Fraction: '.$ntpTimestamp['NtpSecondsFraction'].$endOfLine;
$output .= $prefixLine.'PHP Timestamp : '.$ntpTimestamp['PhpTimestamp'].' - '.date('Y-m-d H:i:s',$ntpTimestamp['PhpTimestamp']).$endOfLine;
return $output;
}
private function dumpUnpackedNtpPacket($ntpPacket, $ntpName, $endOfLine = PHP_EOL)
{
$output = '';
$output .= 'NTP packet of '.$ntpName.$endOfLine;
$output .= '------------------------------------------------------------------------------'.$endOfLine;
$output .= 'Leap Indicator (LI) : '.$ntpPacket['LeapIndicator'];
switch($ntpPacket['LeapIndicator'])
{
case 0: $output .= ' - no warning'; break;
case 1: $output .= ' - last minute has 61 seconds'; break;
case 2: $output .= ' - last minute has 59 seconds'; break;
case 3: $output .= ' - alarm condition (clock not synchronized)'; break;
}
$output .= $endOfLine;
$output .= 'Version Number (VN) : '.$ntpPacket['VersionNumber'].$endOfLine;
$output .= 'Mode : '.$ntpPacket['Mode'];
switch($ntpPacket['Mode'])
{
case 0: $output .= ' - reserved'; break;
case 1: $output .= ' - symmetric active'; break;
case 2: $output .= ' - symmetric passive'; break;
case 3: $output .= ' - client'; break;
case 4: $output .= ' - server (unicast)'; break;
case 5: $output .= ' - broadcast'; break;
case 6: $output .= ' - reserved for NTP control message'; break;
case 7: $output .= ' - reserved for private use'; break;
}
$output .= $endOfLine;
$output .= 'Stratum : '.$ntpPacket['Stratum'];
switch($ntpPacket['Stratum'])
{
case 0: $output .= ' - unspecified or unavailable / kiss-of-death RFC 4330 - SNMP version 4'; break;
case 1: $output .= ' - primary reference'; break;
default:
if ($ntpPacket['Stratum'] >= 2 && $ntpPacket['Stratum'] <= 15)
$output .= ' - secondary reference (synchronized by NTP or SNTP)';
else if ($ntpPacket['Stratum'] >= 16 && $ntpPacket['Stratum'] <= 255)
$output .= ' - reserved';
}
$output .= $endOfLine;
$output .= 'Poll Interval : '.$ntpPacket['PollInterval'].' - '.(2 << $ntpPacket['PollInterval']).' seconds'.$endOfLine;
$output .= 'Precision : '.$ntpPacket['Precision'].$endOfLine;
/*
This is an eight-bit signed integer used as an exponent of
two, where the resulting value is the precision of the system clock
in seconds. This field is significant only in server messages, where
the values range from -6 for mains-frequency clocks to -20 for
microsecond clocks found in some workstations.
*/
$output .= 'Root Delay : '.$ntpPacket['RootDelay'].$endOfLine;
/*
Root Delay: This is a 32-bit signed fixed-point number indicating the
total roundtrip delay to the primary reference source, in seconds
with the fraction point between bits 15 and 16.
*/
$output .= 'Root Dispersion : '.$ntpPacket['RootDispersion'].$endOfLine;
/*
Root Dispersion: This is a 32-bit unsigned fixed-point number
indicating the maximum error due to the clock frequency tolerance, in
seconds with the fraction point between bits 15 and 16. This field
is significant only in server messages, where the values range from
zero to several hundred milliseconds.
*/
$output .= 'Reference Identifier: '.bin2hex($ntpPacket['ReferenceIdentifier']).$endOfLine;
$output .= 'Reference Timestamp'.$endOfLine;
$output .= $this->dumpUnpackedNtpTimestamp($ntpPacket['ReferenceTimestamp'], ' ', $endOfLine);
$output .= 'Originate Timestamp'.$endOfLine;
$output .= $this->dumpUnpackedNtpTimestamp($ntpPacket['OriginateTimestamp'], ' ', $endOfLine);
$output .= 'Receive Timestamp'.$endOfLine;
$output .= $this->dumpUnpackedNtpTimestamp($ntpPacket['ReceiveTimestamp'], ' ', $endOfLine);
$output .= 'Transmit Timestamp'.$endOfLine;
$output .= $this->dumpUnpackedNtpTimestamp($ntpPacket['TransmitTimestamp'], ' ', $endOfLine);
return $output;
}
}
@MircoBabin
Copy link
Author

Why ?

The system clock of my notebook was wrongly set for some reason. I want to detect this situation by retrieving the ntp time and comparing the system clock with the retrieved ntp time.

Because the existing snippets/packages/repositories to retrieve ntp time were cumbersome or did not work. And I wanted a single file and Php 5.4.44 support. That's why I created and published this class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment