summaryrefslogtreecommitdiff
path: root/master/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'master/server/src')
-rw-r--r--master/server/src/networking.ts38
-rw-r--r--master/server/src/server.ts173
-rw-r--r--master/server/src/utility.ts16
3 files changed, 227 insertions, 0 deletions
diff --git a/master/server/src/networking.ts b/master/server/src/networking.ts
new file mode 100644
index 0000000..5a58fcd
--- /dev/null
+++ b/master/server/src/networking.ts
@@ -0,0 +1,38 @@
+import { filter_object } from "utility";
+
+import { NetworkInterfaceInfo, networkInterfaces } from "os";
+
+import { IPSubnetwork, IPv4 } from "ip-matching";
+import { promise as ping_promise } from "ping";
+
+const probe = ping_promise.probe;
+
+export function get_network_interfaces() {
+ const invalid_network_interfaces = [ "lo" ];
+
+ const nw_interfaces =
+ networkInterfaces() as Record<string, NetworkInterfaceInfo[] | undefined>;
+
+ return filter_object(
+ nw_interfaces,
+ (name, opt_info) => {
+ return !invalid_network_interfaces.includes(name) && opt_info !== undefined;
+ }
+ ) as Record<string, NetworkInterfaceInfo[]>;
+}
+
+export async function ping_subnetwork(subnetwork: IPSubnetwork) {
+ const first_ip = subnetwork.getFirst() as IPv4;
+
+ let current_ip = first_ip.getNext();
+
+ while(current_ip !== undefined && subnetwork.matches(current_ip)) {
+ const current_ip_str = current_ip.toString();
+
+ const ping_result = await probe(current_ip_str, { timeout: 0.1 });
+
+ console.log(`${current_ip_str}${ping_result.alive ? " - Alive" : ""}`);
+
+ current_ip = current_ip.getNext();
+ }
+}
diff --git a/master/server/src/server.ts b/master/server/src/server.ts
new file mode 100644
index 0000000..4368b78
--- /dev/null
+++ b/master/server/src/server.ts
@@ -0,0 +1,173 @@
+import { get_network_interfaces, ping_subnetwork } from "networking";
+
+import { exit, argv } from "process";
+import { readdir } from "fs/promises";
+import { join } from "path";
+import { NetworkInterfaceBase } from "os";
+
+import { fastify } from "fastify";
+import fastify_static from "@fastify/static";
+import {
+ toIP as mac_to_ip_address
+} from "@network-utils/arp-lookup";
+import { IPMask, IPv4 } from "ip-matching";
+
+const minion_mac_addresses = [
+ "30:83:98:a4:68:09",
+ "bc:ff:4d:17:62:75"
+];
+
+const minion_positions: Record<string, number> = {
+ "30:83:98:a4:68:09": 13,
+ "bc:ff:4d:17:62:75": 1
+};
+
+async function mac_to_ip_addresses(mac_addresses: string[]) {
+ let failed = [];
+
+ let succeeded = [];
+
+ for(const mac_address of mac_addresses) {
+ const ip_address = await mac_to_ip_address(mac_address);
+
+ if(ip_address === null) {
+ failed.push(mac_address);
+ continue;
+ }
+
+ succeeded.push({ mac_address: mac_address, ip_address: ip_address });
+ }
+
+ return {
+ succeeded: succeeded,
+ failed: failed
+ };
+}
+
+interface NWInterfaceInfo extends NetworkInterfaceBase {
+ family: number | string
+ scopeid?: number;
+}
+
+function get_ipv4_nw_interface_info() {
+ const nw_interfaces = get_network_interfaces();
+
+ const nw_interface_name = Object.keys(nw_interfaces)[0];
+
+ const nw_interface_info_arr = nw_interfaces[nw_interface_name] as NWInterfaceInfo[];
+
+ const nw_interface_info = nw_interface_info_arr.find(info => {
+ return info.family === "IPv4" || info.family === 4;
+ });
+
+ if(nw_interface_info === undefined) {
+ console.error(
+ `Error: Network interface "${nw_interface_name}" doesn't support IPv4"`
+ );
+ exit(1);
+ }
+
+ return nw_interface_info;
+}
+
+function get_current_subnetwork(nw_interface_info: NWInterfaceInfo) {
+ const ip_mask = new IPMask(
+ new IPv4(nw_interface_info.address),
+ new IPv4(nw_interface_info.netmask)
+ );
+
+ const subnetwork = ip_mask.convertToSubnet();
+
+ if(subnetwork === undefined) {
+ console.error(`Error: Failed to convert ${ip_mask.input} to subnet`);
+ exit(1);
+ }
+
+ return subnetwork;
+}
+
+async function get_minion_ip_addresses(nw_interface_info: NWInterfaceInfo) {
+ let mac_to_ip_result = await mac_to_ip_addresses(minion_mac_addresses);
+
+ if(mac_to_ip_result.failed.length === 0) {
+ return mac_to_ip_result.succeeded;
+ }
+
+ console.log(
+ "Warning: Couldn't find all minion IP addresses in ARP table. " +
+ "Updating ARP table entries..."
+ );
+
+ const subnetwork = get_current_subnetwork(nw_interface_info);
+
+ await ping_subnetwork(subnetwork);
+
+ console.log("Retrying finding minion IP addresses in ARP table...");
+
+ mac_to_ip_result = await mac_to_ip_addresses(minion_mac_addresses);
+
+ if(mac_to_ip_result.failed.length !== 0) {
+ console.error(
+ "Error: Failed to find all minion IP addresses in ARP table " +
+ "even after updating ARP table entries"
+ );
+ exit(1);
+ }
+
+ return mac_to_ip_result.succeeded;
+}
+
+async function main() {
+ const nw_interface_info = get_ipv4_nw_interface_info();
+
+ const minion_info = (await get_minion_ip_addresses(nw_interface_info)).map(
+ ({ mac_address, ip_address }) => {
+ return {
+ ip_address: ip_address,
+ position: minion_positions[mac_address]
+ };
+ }
+ );
+
+ const app = fastify();
+
+ const is_prod = argv.length !== 0 && argv.includes("--prod");
+
+ if(is_prod) {
+ const dist_dir_path = join(__dirname, "/../../client/dist");
+
+ await readdir(dist_dir_path).catch(() => {
+ console.error("Error: The client dist directory doesn't exist");
+ exit(1);
+ });
+
+ app.register(fastify_static, { root: dist_dir_path });
+
+ app.route({
+ method: "GET",
+ url: "/",
+ handler: (req, reply) => {
+ reply.sendFile("index.html");
+ }
+ });
+
+ }
+
+ app.route({
+ method: "GET",
+ url: "/api/v1/minions",
+ handler: (req, reply) => {
+ reply.send({ data: minion_info });
+ }
+ });
+
+ const address = nw_interface_info.address;
+
+ const port = 80;
+
+ await app.listen(port, address);
+
+ console.log(`Listening on address http://${address}:${port}`);
+}
+
+main();
diff --git a/master/server/src/utility.ts b/master/server/src/utility.ts
new file mode 100644
index 0000000..6a02575
--- /dev/null
+++ b/master/server/src/utility.ts
@@ -0,0 +1,16 @@
+export function filter_object<
+ ObjectType,
+ Key extends keyof ObjectType,
+ Value extends ObjectType[keyof ObjectType]
+>(
+ obj: ObjectType,
+ filter_cb: (key: Key, value: Value) => boolean
+) {
+ const entries = Object.entries(obj);
+
+ const filtered = entries.filter(([ str_key, any_value ]) => {
+ return filter_cb(str_key as Key, any_value as Value);
+ });
+
+ return Object.fromEntries(filtered) as ObjectType;
+}