diff options
Diffstat (limited to 'master/server/src')
-rw-r--r-- | master/server/src/networking.ts | 38 | ||||
-rw-r--r-- | master/server/src/server.ts | 173 | ||||
-rw-r--r-- | master/server/src/utility.ts | 16 |
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; +} |