-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create NTP Client class. Added ntp-packet utils to create and parse packets. Added global constants as per RFC5905. * Added parsing for timestamps on reply messages. * Added calculation for offset and delta. * Fixed offset and rtt calculation. * Added command for setting system time. * Renamed client.ts to ntp-client.ts * Added JSDOC. Added command line tool in index.ts for setting system time. Removed unwanted constants. * Added tests * Added README.md Update method names for NtpClient class Added method tto calculated correct timein NtpClient class * Added comments * Added link to Challenge 28 in global README
- Loading branch information
1 parent
2c5adcc
commit 416e93c
Showing
7 changed files
with
404 additions
and
0 deletions.
There are no files selected for viewing
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Challenge 28 - Write Your Own NTP Client | ||
|
||
This challenge corresponds to the 28<sup>th</sup> part of the Coding Challenges series by John Crickett https://codingchallenges.fyi/challenges/challenge-ntp. | ||
|
||
## Description | ||
|
||
The objective of the challenge is to build a NTP client that can find the correct system time using the Network Time Protocol defined in [RFC5905](https://datatracker.ietf.org/doc/html/rfc5905). | ||
|
||
## Usage | ||
|
||
You can use the `ts-node` tool to run the command line version of the NTP Client as follows: | ||
|
||
```bash | ||
npx ts-node <path/to/index.ts> [--setTime] | ||
``` | ||
|
||
### Options | ||
|
||
- `--setTime`: If true, then the system time will be updated. It currently supports Linux systems only. | ||
|
||
Or you can directly use the `NtpClient` class defined in [ntp-client.ts](ntp-client.ts) as follows: | ||
|
||
```typescript | ||
import { NtpClient } from '<path/to/ntp-client.ts>'; | ||
|
||
const server = '0.pool.ntp.org'; | ||
const timeout = 5000; | ||
|
||
const client = new NtpClient(server, timeout); | ||
client | ||
.getTime() | ||
.then((msg) => { | ||
console.log('NTP Packet', msg); | ||
|
||
const offset = client.getOffset(); | ||
console.log(`Offset (theta) ${offset}ms`); | ||
|
||
const rtt = client.getRtt(); | ||
console.log(`RTT (delta) ${rtt}ms`); | ||
|
||
// Calculate the correct time as per server's response and offset calculation | ||
const now = client.now(); | ||
|
||
// Set system time | ||
client | ||
.setSystemTime(now) | ||
.then(() => { | ||
console.log('Successfully set system time'); | ||
}) | ||
.catch((err) => { | ||
console.error(err); | ||
}); | ||
}) | ||
.catch((err) => { | ||
console.error(err); | ||
}); | ||
``` | ||
|
||
## Run tests | ||
|
||
To run the tests for the NTP Client, go to the root directory of this repository and run the following command: | ||
|
||
```bash | ||
npm test src/28/ | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { NTP_VERSION } from '../constants'; | ||
import { NtpClient } from '../ntp-client'; | ||
|
||
describe('Testing NTP client', () => { | ||
const timeout = 5000; | ||
const server = '0.pool.ntp.org'; | ||
|
||
it('should should send NTP request and wait for reply', (done) => { | ||
const client = new NtpClient(server, timeout); | ||
client.getTime().then((packet) => { | ||
expect(packet.leap).toBe(0); | ||
expect(packet.version).toBe(NTP_VERSION); | ||
expect(packet.mode).toBe(4); | ||
|
||
expect(() => client.getOffset()).not.toThrow(); | ||
expect(() => client.getRtt()).not.toThrow(); | ||
expect(() => client.now()).not.toThrow(); | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
it('should timeout if no packet received', (done) => { | ||
const client = new NtpClient(server, 10); | ||
client.getTime().catch((err) => { | ||
expect(err.toString()).toContain('Timeout'); | ||
done(); | ||
}); | ||
}); | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Global constants as per RFC5905 | ||
// https://datatracker.ietf.org/doc/html/rfc5905#section-7.2 | ||
export const NTP_VERSION = 4; | ||
export const PORT = 123; | ||
|
||
export const POW_2_32 = Math.pow(2, 32); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { program } from 'commander'; | ||
import { NtpClient } from './ntp-client'; | ||
|
||
const client = new NtpClient(); | ||
|
||
program.option('--setTime', 'Set the system timestamp'); | ||
|
||
program.parse(); | ||
|
||
const options = program.opts(); | ||
|
||
client | ||
.getTime() | ||
.then((msg) => { | ||
console.log('NTP Packet', msg); | ||
|
||
const offset = client.getOffset(); | ||
console.log(`Offset (theta) ${offset}ms`); | ||
|
||
const rtt = client.getRtt(); | ||
console.log(`RTT (delta) ${rtt}ms`); | ||
|
||
// The correct time will be calculated based on the offset | ||
const now = client.now(); | ||
|
||
if (options.setTime) { | ||
client | ||
.setSystemTime(now) | ||
.then(() => { | ||
console.log('Successfully set system time'); | ||
}) | ||
.catch((err) => { | ||
console.error(err); | ||
}); | ||
} | ||
}) | ||
.catch((err) => { | ||
console.error(err); | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import UDP from 'dgram'; | ||
import { | ||
NtpPacket, | ||
calculateOffset, | ||
calculateRoundTripDelay, | ||
createNtpPacket, | ||
parseNtpPacket | ||
} from './ntp-packet'; | ||
import { PORT } from './constants'; | ||
import { exec } from 'child_process'; | ||
import os from 'os'; | ||
|
||
export class NtpClient { | ||
/** | ||
* The NTP server. | ||
* | ||
* @type {string} | ||
*/ | ||
server: string; | ||
|
||
/** | ||
* Timeout for the request. | ||
* | ||
* @type {number} | ||
*/ | ||
timeout: number; | ||
client: UDP.Socket; | ||
replyPacket?: NtpPacket; | ||
|
||
constructor(server?: string, timeout?: number) { | ||
this.server = server ?? '0.pool.ntp.org'; | ||
this.timeout = timeout ?? 10000; | ||
this.client = UDP.createSocket('udp4'); | ||
} | ||
|
||
/** | ||
* This function sends a NTP request packet and waits for the reply. | ||
* If no reply is sent by the server within the timeout period specified, | ||
* the promise is rejected. | ||
* | ||
* @returns {Promise<NtpPacket>} | ||
*/ | ||
getTime(): Promise<NtpPacket> { | ||
return new Promise<NtpPacket>((res, rej) => { | ||
// If no message received in the given timeout period, | ||
// then the client is closed and Promise is rejected. | ||
const timeout = setTimeout(() => { | ||
this.client.close(); | ||
rej('Timeout waiting for NTP reply packet!'); | ||
}, this.timeout); | ||
|
||
const packet = createNtpPacket(); | ||
|
||
this.client.on('error', (err) => { | ||
clearTimeout(timeout); | ||
this.client.close(); | ||
rej(err); | ||
}); | ||
|
||
this.client.send(packet, PORT, this.server, (err) => { | ||
if (err !== null) { | ||
clearTimeout(timeout); | ||
this.client.close(); | ||
rej(err); | ||
return; | ||
} | ||
|
||
// After sending the request packet, check for a message | ||
this.client.once('message', (msg) => { | ||
const packet = parseNtpPacket(msg); | ||
this.replyPacket = packet; | ||
clearTimeout(timeout); | ||
this.client.close(); | ||
res(packet); | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
getOffset(): number { | ||
if (!this.replyPacket) { | ||
throw new Error('Invalid replyPacket'); | ||
} | ||
return calculateOffset(this.replyPacket); | ||
} | ||
|
||
getRtt(): number { | ||
if (!this.replyPacket) { | ||
throw new Error('Invalid replyPacket'); | ||
} | ||
return calculateRoundTripDelay(this.replyPacket); | ||
} | ||
|
||
/** | ||
* Returns the correct time as per the server response and offset. | ||
* | ||
* @returns {Date} | ||
*/ | ||
now(): Date { | ||
if (!this.replyPacket) { | ||
throw new Error('Invalid replyPacket'); | ||
} | ||
const date = new Date(); | ||
date.setTime(date.getTime() + this.getOffset()); | ||
return date; | ||
} | ||
|
||
setSystemTime(date: Date): Promise<void> { | ||
return new Promise<void>((res, rej) => { | ||
const platform = os.platform(); | ||
if (platform === 'linux') { | ||
exec( | ||
`/bin/date --set="${date.toISOString()}"`, | ||
(err, stdout, stderr) => { | ||
if (err) { | ||
rej(err); | ||
} else if (stderr) { | ||
rej(stderr); | ||
} else { | ||
res(); | ||
} | ||
} | ||
); | ||
} else { | ||
rej(`Setting system time for ${platform} is currently not supported`); | ||
} | ||
}); | ||
} | ||
} |
Oops, something went wrong.