Created
October 16, 2022 18:04
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.