Skip to content

Commit

Permalink
Challenge 28 - Ntp client (#41)
Browse files Browse the repository at this point in the history
* 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
jainmohit2001 authored Oct 13, 2023
1 parent 2c5adcc commit 416e93c
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Checkout my [Notion](https://mohitjain.notion.site/Coding-Challenges-af9b8197a43
25. [Write Your Own NATS Message Broker](src/25/)
26. [Write Your Own Git](src/26/)
27. [Write Your Own Rate Limiter](src/27/)
28. [Write Your Own NTP Client](src/28/)

## Installation

Expand Down
65 changes: 65 additions & 0 deletions src/28/README.md
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/
```
30 changes: 30 additions & 0 deletions src/28/__tests__/ntp-client.test.ts
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();
});
});
});
6 changes: 6 additions & 0 deletions src/28/constants.ts
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);
39 changes: 39 additions & 0 deletions src/28/index.ts
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);
});
129 changes: 129 additions & 0 deletions src/28/ntp-client.ts
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`);
}
});
}
}
Loading

0 comments on commit 416e93c

Please sign in to comment.