// What the device sheet shows of a device, read straight from the document: // the fields of FILIUS' settings panel of its kind (JHostKonfiguration, // JSwitchKonfiguration, JModemKonfiguration, JVermittlungsrechnerKonfiguration // with its forwarding table, JGatewayConfiguration). import * as Y from 'yjs'; import { defaultSwitchConfiguration, DEFAULT_SWITCH_RETENTION_MILLISECONDS, dhcpServerConfiguration, firewallConfiguration, forwardsIPv4ByDefault, type FirewallConfiguration, parsePortForwardingRow, routeTableRows, type DhcpServerConfiguration, type GatewayPortForwardingRow, stableGeneratedMacAddress, type HostLabelPolicy, type ManualRoute, type NodeKind, type RemoteLinkRelay, type RouteRow, } from '@mdg-filius/core'; import { modemPort } from '@mdg-filius/fls'; import type { ModemConnection } from './settings.js'; import { inOrder, ROOTS } from './projectDoc.js'; import { fromY } from './values.js'; import { cardAddress, nodeLabel } from './view.js'; /** A device's DHCP server as Java's dialog shows it (JDHCPKonfiguration). */ export interface DhcpServerSheet { readonly isActive: boolean; readonly lowerBoundIPAddress: string; readonly upperBoundIPAddress: string; /** The device's own mask (the LAN card's on a home router), shown, not changed. */ readonly subnetMask: string; /** What the server hands out (DHCPServer.determineGatewayip): its own setting, the device's gateway (0.0.0.0 without one), a home router's LAN address. */ readonly gateway: string; readonly gatewayEditable: boolean; /** determineDnsserverip: its own setting (always on a home router), else the device's DNS server or 0.0.0.0. */ readonly dnsServer: string; readonly dnsServerEditable: boolean; readonly useOwnSettings: boolean; /** A home router's dialog leaves the choice without effect; the sheet offers it on a PC or notebook only. */ readonly ownSettingsOffered: boolean; readonly staticAssignments: readonly { readonly macAddress: string; readonly ipAddress: string }[]; } export interface HostSheet { readonly kind: 'pc' | 'notebook'; readonly name: string; /** Host.useIPAsName and useMACAsName. */ readonly labels: { readonly ipAddress: boolean; readonly macAddress: boolean }; /** The card's MAC address (the model keeps them in upper case, as Java shows them). */ readonly macAddress: string; readonly ipAddress: string; readonly subnetMask: string; readonly defaultGateway: string; readonly dnsServer: string; readonly dhcpClient: boolean; readonly ipForwarding: boolean; readonly wireless: boolean; /** The SSID of the network the card joins, '' for none. */ readonly ssid: string; /** Java offers the wireless card only while the cable's port is free, or the card is wireless already. */ readonly wirelessPossible: boolean; /** The switches' SSIDs, in the order of the devices, each once: the networks to choose from. */ readonly networks: readonly string[]; readonly dhcpServer: DhcpServerSheet; } export interface SwitchSheet { readonly kind: 'networkSwitch'; readonly name: string; readonly ssid: string; readonly retentionSeconds: number; readonly cloud: boolean; } export interface ModemSheet { readonly kind: 'remoteLink'; readonly name: string; /** The pair in the project: two modems on one port. */ readonly port: number; /** How it finds its partner (setModemConnection). */ readonly connection: ModemConnection; /** Joining: the code of the modem it connects to ('' while none is given). */ readonly code: string; } /** A router's or home router's card. */ export interface InterfaceSheet { readonly portId: string; readonly ipAddress: string; readonly subnetMask: string; readonly macAddress: string; /** * The device at the other end of its cable (its label, as Java names it), * the address of the card the cable plugs into there (a computer's, a * router's; null for a switch or a modem, which have none) and the cable; * null without one. */ readonly connected: { readonly device: string; readonly address: string | null; readonly link: string } | null; } /** A router's or home router's firewall as Java's dialog shows it (JFirewallDialog). */ export type FirewallSheet = Pick; function firewallSheet(node: Y.Map): FirewallSheet { const { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules } = firewallConfiguration((fromY(settingsOf(node).get('firewall')) ?? {}) as Partial); return { isActive, dropIcmp, filterSynSegmentsOnly, defaultPolicy, rules }; } export interface RouterSheet { readonly kind: 'router'; readonly name: string; readonly gateway: string; readonly rip: boolean; /** Shown, not changed: Java's panel greys it out. */ readonly ipForwarding: boolean; readonly interfaces: readonly InterfaceSheet[]; /** The manual rows, in their order. */ readonly routes: readonly ManualRoute[]; readonly firewall: FirewallSheet; /** The whole table as Java shows it (Weiterleitungstabelle.holeTabelle): own addresses and networks, localhost, the manual rows, the gateway. */ readonly table: readonly RouteRow[]; } export interface HomeRouterSheet { readonly kind: 'gateway'; readonly name: string; readonly gateway: string; readonly ipForwarding: boolean; /** DHCP for the WAN card. */ readonly dhcpClient: boolean; readonly wan: InterfaceSheet; readonly lan: InterfaceSheet; readonly dhcpServer: DhcpServerSheet; readonly firewall: FirewallSheet; /** The port forwarding rows, each with whether Java's firmware forwards it (GatewayFirmware.validateData). */ readonly portForwarding: readonly (GatewayPortForwardingRow & { readonly forwarded: boolean })[]; } export interface NameSheet { readonly kind: 'unsupported'; readonly name: string; } export type DeviceSheet = HostSheet | SwitchSheet | ModemSheet | RouterSheet | HomeRouterSheet | NameSheet; const settingsOf = (node: Y.Map) => node.get('settings') as Y.Map; const setting = (node: Y.Map, key: string) => settingsOf(node).get(key) as Y.Map | undefined; const text = (map: Y.Map | undefined, key: string) => (map?.get(key) as string | undefined) ?? ''; /** A router's or home router's cards, each with what its cable joins. */ function interfacesOf(doc: Y.Doc, id: string, node: Y.Map): InterfaceSheet[] { const all = doc.getMap>(ROOTS.nodes); const links = inOrder(doc.getMap>(ROOTS.links)); return (node.get('ports') as Y.Array>).toArray().map((port) => { const portId = port.get('id') as string; const config = port.get('config') as Y.Map | undefined; const cable = links.find(([, link]) => (link.get('sourceNode') === id && link.get('sourcePort') === portId) || (link.get('targetNode') === id && link.get('targetPort') === portId)); const near = cable !== undefined && cable[1].get('sourceNode') === id && cable[1].get('sourcePort') === portId; const other = cable === undefined ? undefined : all.get(cable[1].get(near ? 'targetNode' : 'sourceNode') as string); return { portId, ipAddress: text(config, 'ipAddress'), subnetMask: text(config, 'subnetMask'), macAddress: (port.get('mac') as string | undefined) ?? stableGeneratedMacAddress(portId), connected: cable === undefined || other === undefined ? null : { device: nodeLabel(other), address: cardAddress(other, cable[1].get(near ? 'targetPort' : 'sourcePort') as string), link: cable[0] }, }; }); } /** The DHCP server's sheet, from the device's own address settings (or a home router's LAN card). */ function dhcpServerSheet(node: Y.Map, own: { readonly subnetMask: string; readonly gateway: string; readonly dnsServer: string }, home: boolean): DhcpServerSheet { const stored = (fromY(settingsOf(node).get('dhcpServer')) ?? {}) as Partial; const server = dhcpServerConfiguration(stored); const unset = (value: string) => (value === '' ? '0.0.0.0' : value); const ownSettings = !home && server.useOwnSettings; return { isActive: server.isActive, lowerBoundIPAddress: server.lowerBoundIPAddress, upperBoundIPAddress: server.upperBoundIPAddress, subnetMask: own.subnetMask, gateway: home ? own.gateway : ownSettings ? server.gatewayIPAddress : unset(own.gateway), gatewayEditable: ownSettings, dnsServer: home || ownSettings ? server.dnsServerIPAddress : unset(own.dnsServer), dnsServerEditable: home || ownSettings, useOwnSettings: server.useOwnSettings, ownSettingsOffered: !home, staticAssignments: server.staticAssignments.map(({ macAddress, ipAddress }) => ({ macAddress, ipAddress })), }; } /** The sheet of a device, or undefined when the document has no such device. */ export function deviceSheet(doc: Y.Doc, id: string): DeviceSheet | undefined { const all = doc.getMap>(ROOTS.nodes); const node = all.get(id); if (node === undefined) return undefined; const kind = node.get('kind') as NodeKind; const name = node.get('name') as string; switch (kind) { case 'pc': case 'notebook': { const device = setting(node, 'device'); const wireless = setting(node, 'wireless'); const port = (node.get('ports') as Y.Array>).get(0); const portId = port?.get('id') as string | undefined; const plugged = [...doc.getMap>(ROOTS.links).values()].some((link) => link.get('sourceNode') === id || link.get('targetNode') === id); const policy = node.get('labelPolicy') as HostLabelPolicy; const isWireless = wireless?.get('isEnabled') === true; const networks = new Set(); for (const [other, candidate] of inOrder(all)) { if (candidate.get('kind') !== 'networkSwitch') continue; networks.add((setting(candidate, 'switch')?.get('ssid') as string | undefined) ?? defaultSwitchConfiguration(other).ssid); } return { kind, name, labels: { ipAddress: policy === 'ipAddress' || policy === 'ipAndMAC', macAddress: policy === 'macAddress' || policy === 'ipAndMAC' }, macAddress: (port?.get('mac') as string | undefined) ?? (portId === undefined ? '' : stableGeneratedMacAddress(portId)), ipAddress: (device?.get('ipAddress') as string | undefined) ?? '', subnetMask: (device?.get('subnetMask') as string | undefined) ?? '', defaultGateway: (device?.get('defaultGateway') as string | undefined) ?? '', dnsServer: (device?.get('dnsServer') as string | undefined) ?? '', dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true, ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind), wireless: isWireless, ssid: (wireless?.get('ssid') as string | undefined) ?? '', wirelessPossible: isWireless || !plugged, networks: [...networks], dhcpServer: dhcpServerSheet(node, { subnetMask: text(device, 'subnetMask'), gateway: text(device, 'defaultGateway'), dnsServer: text(device, 'dnsServer') }, false), }; } case 'networkSwitch': { const config = setting(node, 'switch'); return { kind, name, ssid: (config?.get('ssid') as string | undefined) ?? defaultSwitchConfiguration(id).ssid, // Whole seconds, as Java's panel shows them (retentionTime / 1000 in long arithmetic). retentionSeconds: Math.trunc(((config?.get('retentionTimeMilliseconds') as number | undefined) ?? DEFAULT_SWITCH_RETENTION_MILLISECONDS) / 1000), cloud: config?.get('showsAsCloud') === true, }; } case 'remoteLink': { const config = setting(node, 'remoteLink'); const relay = fromY(config?.get('relay')) as RemoteLinkRelay | undefined; return { kind, name, port: modemPort(config?.get('pairIdentifier') as string | undefined), connection: relay?.role ?? 'inProject', code: relay?.role === 'join' ? relay.code : '', }; } case 'router': { const interfaces = interfacesOf(doc, id, node); const gateway = text(setting(node, 'device'), 'defaultGateway'); const routes = (fromY(settingsOf(node).get('routes')) ?? []) as unknown as ManualRoute[]; return { kind, name, gateway, rip: settingsOf(node).get('rip') === true, ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind), interfaces, routes, table: routeTableRows(interfaces, routes, gateway), firewall: firewallSheet(node), }; } case 'gateway': { const [wan, lan] = interfacesOf(doc, id, node); const rows = (fromY(settingsOf(node).get('portForwarding')) ?? []) as unknown as GatewayPortForwardingRow[]; return { kind, name, gateway: text(setting(node, 'device'), 'defaultGateway'), ipForwarding: (settingsOf(node).get('ipForwarding') as boolean | undefined) ?? forwardsIPv4ByDefault(kind), dhcpClient: setting(node, 'dhcpClient')?.get('isEnabled') === true, wan: wan!, lan: lan!, // Java's home router hands out its LAN address as the gateway, with the LAN card's mask. dhcpServer: dhcpServerSheet(node, { subnetMask: lan!.subnetMask, gateway: lan!.ipAddress, dnsServer: '' }, true), firewall: firewallSheet(node), portForwarding: rows.map((row) => ({ ...row, forwarded: parsePortForwardingRow(row) !== undefined })), }; } case 'unsupported': return { kind, name }; } }