Skip to content

Commit 0fa3e0c

Browse files
authored
feat: implement HTTP client program (#175)
Related to #121 This PR implements an HTTP request program. For now it's just a single TCP packet, but we should come up with a better way to show this.
1 parent 6b44fa6 commit 0fa3e0c

File tree

6 files changed

+106
-15
lines changed

6 files changed

+106
-15
lines changed

src/graphics/renderables/program_info.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { DeviceId } from "../../types/graphs/datagraph";
2+
import { ViewGraph } from "../../types/graphs/viewgraph";
13
import { createDropdown, Renderable } from "../right_bar";
24

35
interface HasValue {
@@ -13,6 +15,10 @@ export class ProgramInfo implements Renderable {
1315
this.name = name;
1416
}
1517

18+
withDestinationDropdown(viewgraph: ViewGraph, srcId: DeviceId) {
19+
this.withDropdown("Destination", otherDevices(viewgraph, srcId));
20+
}
21+
1622
withDropdown(name: string, options: { value: string; text: string }[]) {
1723
const dropdown = createDropdown(name, options);
1824
this.inputs.push(dropdown);
@@ -27,3 +33,10 @@ export class ProgramInfo implements Renderable {
2733
return this.inputs;
2834
}
2935
}
36+
37+
function otherDevices(viewgraph: ViewGraph, srcId: DeviceId) {
38+
return viewgraph
39+
.getDeviceIds()
40+
.filter((id) => id !== srcId)
41+
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));
42+
}

src/programs/echo_sender.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,6 @@ import { IPv4Packet } from "../packets/ip";
99
import { NetworkDevice } from "../types/devices";
1010
import { EthernetFrame } from "../packets/ethernet";
1111

12-
function adjacentDevices(viewgraph: ViewGraph, srcId: DeviceId) {
13-
const adjacentDevices = viewgraph
14-
.getDeviceIds()
15-
.filter((id) => id !== srcId)
16-
.map((id) => ({ value: id.toString(), text: `Device ${id}` }));
17-
18-
return adjacentDevices;
19-
}
20-
2112
export class SingleEcho extends ProgramBase {
2213
static readonly PROGRAM_NAME = "Send ICMP echo";
2314

@@ -50,9 +41,8 @@ export class SingleEcho extends ProgramBase {
5041
return;
5142
}
5243
if (
53-
!(
54-
srcDevice instanceof NetworkDevice && dstDevice instanceof NetworkDevice
55-
)
44+
!(srcDevice instanceof NetworkDevice) ||
45+
!(dstDevice instanceof NetworkDevice)
5646
) {
5747
console.log(
5848
"At least one device between source and destination is not a network device",
@@ -78,7 +68,7 @@ export class SingleEcho extends ProgramBase {
7868

7969
static getProgramInfo(viewgraph: ViewGraph, srcId: DeviceId): ProgramInfo {
8070
const programInfo = new ProgramInfo(this.PROGRAM_NAME);
81-
programInfo.withDropdown("Destination", adjacentDevices(viewgraph, srcId));
71+
programInfo.withDestinationDropdown(viewgraph, srcId);
8272
return programInfo;
8373
}
8474
}
@@ -137,7 +127,7 @@ export class EchoServer extends ProgramBase {
137127
];
138128

139129
const programInfo = new ProgramInfo(this.PROGRAM_NAME);
140-
programInfo.withDropdown("Destination", adjacentDevices(viewgraph, srcId));
130+
programInfo.withDestinationDropdown(viewgraph, srcId);
141131
programInfo.withDropdown("Time between pings", delayOptions);
142132
return programInfo;
143133
}

src/programs/http_client.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ProgramInfo } from "../graphics/renderables/device_info";
2+
import { EthernetFrame } from "../packets/ethernet";
3+
import { IPv4Packet } from "../packets/ip";
4+
import { Flags, TcpSegment } from "../packets/tcp";
5+
import { NetworkDevice } from "../types/devices";
6+
import { DeviceId } from "../types/graphs/datagraph";
7+
import { ViewGraph } from "../types/graphs/viewgraph";
8+
import { sendRawPacket } from "../types/packet";
9+
import { ProgramBase } from "./program_base";
10+
11+
export class HttpClient extends ProgramBase {
12+
static readonly PROGRAM_NAME = "Send HTTP request";
13+
14+
private dstId: DeviceId;
15+
16+
protected _parseInputs(inputs: string[]): void {
17+
if (inputs.length !== 1) {
18+
console.error(
19+
"HttpClient requires 1 input. " + inputs.length + " were given.",
20+
);
21+
return;
22+
}
23+
this.dstId = parseInt(inputs[0]);
24+
}
25+
26+
protected _run() {
27+
this.sendHttpRequest();
28+
this.signalStop();
29+
}
30+
31+
protected _stop() {
32+
// Nothing to do
33+
}
34+
35+
private sendHttpRequest() {
36+
const dstDevice = this.viewgraph.getDevice(this.dstId);
37+
const srcDevice = this.viewgraph.getDevice(this.srcId);
38+
if (!dstDevice) {
39+
console.error("Destination device not found");
40+
return;
41+
}
42+
if (
43+
!(srcDevice instanceof NetworkDevice) ||
44+
!(dstDevice instanceof NetworkDevice)
45+
) {
46+
console.log(
47+
"At least one device between source and destination is not a network device",
48+
);
49+
return;
50+
}
51+
52+
// Encode dummy HTTP request
53+
const httpRequest = "GET / HTTP/1.1\r\nHost: " + dstDevice.ip + "\r\n\r\n";
54+
const content = new TextEncoder().encode(httpRequest);
55+
56+
// Random number between 1024 and 65535
57+
const srcPort = Math.floor(Math.random() * (65535 - 1024) + 1024);
58+
const flags = new Flags();
59+
60+
// Wrap in TCP segment
61+
const payload = new TcpSegment(srcPort, 80, 0, 0, flags, content);
62+
63+
// Wrap in IP packet
64+
const ipPacket = new IPv4Packet(srcDevice.ip, dstDevice.ip, payload);
65+
const path = this.viewgraph.getPathBetween(this.srcId, this.dstId);
66+
let dstMac = dstDevice.mac;
67+
if (!path) return;
68+
for (const id of path.slice(1)) {
69+
const device = this.viewgraph.getDevice(id);
70+
// if there’s a router in the middle, first send frame to router mac
71+
if (device instanceof NetworkDevice) {
72+
dstMac = device.mac;
73+
break;
74+
}
75+
}
76+
const ethernetFrame = new EthernetFrame(srcDevice.mac, dstMac, ipPacket);
77+
sendRawPacket(this.viewgraph, this.srcId, ethernetFrame);
78+
}
79+
80+
static getProgramInfo(viewgraph: ViewGraph, srcId: DeviceId): ProgramInfo {
81+
const programInfo = new ProgramInfo(this.PROGRAM_NAME);
82+
programInfo.withDestinationDropdown(viewgraph, srcId);
83+
return programInfo;
84+
}
85+
}

src/programs/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ProgramInfo } from "../graphics/renderables/program_info";
22
import { DeviceId } from "../types/graphs/datagraph";
33
import { ViewGraph } from "../types/graphs/viewgraph";
44
import { EchoServer, SingleEcho } from "./echo_sender";
5+
import { HttpClient } from "./http_client";
56

67
export type Pid = number;
78

@@ -63,7 +64,7 @@ type ProgramConstructor = new (
6364
// - Have a static readonly PROGRAM_NAME property
6465
// - Have a constructor with the signature (viewgraph, srcId, inputs)
6566
// - Have a getProgramInfo static method
66-
const programList = [SingleEcho, EchoServer];
67+
const programList = [SingleEcho, EchoServer, HttpClient];
6768

6869
// Map of program name to program constructor
6970
const programMap = new Map<string, ProgramConstructor>(

src/types/packet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const contextPerPacketType: Record<string, GraphicsContext> = {
2020
IP: circleGraphicsContext(Colors.Green, 0, 0, 5),
2121
"ICMP-8": circleGraphicsContext(Colors.Red, 0, 0, 5),
2222
"ICMP-0": circleGraphicsContext(Colors.Yellow, 0, 0, 5),
23+
TCP: circleGraphicsContext(Colors.Hazel, 0, 0, 5), // for HTTP
2324
};
2425

2526
const highlightedPacketContext = circleGraphicsContext(Colors.Violet, 0, 0, 6);

src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export enum Colors {
1010
White = 0xffffff,
1111
Black = 0x000000,
1212
Yellow = 0xffff00,
13+
Hazel = 0xd99802,
1314
}
1415

1516
export function circleGraphicsContext(

0 commit comments

Comments
 (0)