import { useState, useEffect, Fragment, useCallback } from "react"; import { Button } from "@/components/Button"; import TextArea from "@/components/TextArea"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import { MdOutlineDelete, MdOutlineNorth, MdOutlinePlusOne, MdOutlineSouth } from "react-icons/md"; import { SettingsPageHeader } from "@components/SettingsPageheader"; import notifications from "../notifications"; import { SelectMenuBasic } from "../components/SelectMenuBasic"; import { SettingsItem } from "./devices.$id.settings"; import { Checkbox } from "@/components/Checkbox"; import { GridCard } from "@components/Card"; import InputField, { FieldError, InputFieldWithLabel } from "@components/InputField"; import FieldLabel from "@components/FieldLabel"; import { v4 as uuidv4 } from 'uuid' /** * Switch channel command definition (what should be sent if channel is selected) */ export interface SwitchChannelCommands { // Remote address address: string; // Protocol to send data in protocol: "tcp" | "udp" | "http" | "https"; // Command format format: "hex" | "base64" | "ascii" | "http-raw"; // Comma separated commands commands: string; } /** * Switch channel definition */ export interface SwitchChannel { name: string; id: string; commands: SwitchChannelCommands[]; } export const CommandsTextHelp: Record = { hex: "Provide comma separated list of commands, e.g. 0x24,0x68,0xA40A", base64: "Provide comma separated list of commands in base64, e.g. aGVsbG8=,d29ybGQ=", ascii: "Provide newline separated list of commands, e.g. hello\nworld", "http-raw": "Provide raw HTTP request, e.g. GET / HTTP/1.1", } export const GetCompatibleCommands = (protocol: SwitchChannelCommands["protocol"]) => { if (protocol == "http" || protocol == "https") { return ["http-raw"]; } else { return ["hex", "base64", "ascii"]; } } export const CommandsTextPlaceholder: Record = { hex: "0x24,0x68,0xA4", base64: "aGVsbG8=,d29ybGQ=", ascii: "hello\nworld", "http-raw": `GET /images HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Connection: close` } export const GenerateSwitchChannelId = () => { return uuidv4(); } export default function SettingsSwitchRoute() { const [send] = useJsonRpc(); const [kvmSwitchEnabled, setKvmSwitchEnabled] = useState(null); const [switchChannels, setSwitchChannels] = useState([]); const updateSwitchChannelData = (index: number, data: SwitchChannel) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels[index] = data; setSwitchChannels(newSwitchChannels); }; const updateSwitchChannelCommands = (index: number, channelIndex: number, data: SwitchChannelCommands) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels[index].commands[channelIndex] = data; setSwitchChannels(newSwitchChannels); }; const removeChannelById = (index: number) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels.splice(index, 1); setSwitchChannels(newSwitchChannels); }; const removeCommandById = (index: number, channelIndex: number) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels[index].commands.splice(channelIndex, 1); setSwitchChannels(newSwitchChannels); }; const addChannel = (afterIndex: number) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels.splice(afterIndex + 1, 0, { name: "", id: GenerateSwitchChannelId(), commands: [ { address: "", protocol: "tcp", format: "hex", commands: "", }, ], }); setSwitchChannels(newSwitchChannels); }; const addCommand = (channelIndex: number, afterIndex: number) => { const newSwitchChannels = [...switchChannels]; newSwitchChannels[channelIndex].commands.splice(afterIndex + 1, 0, { address: "", protocol: "tcp", format: "hex", commands: "", }); setSwitchChannels(newSwitchChannels); } const moveChannel = (fromIndex: number, moveUp: boolean) => { const newSwitchChannels = [...switchChannels]; const channel = newSwitchChannels[fromIndex]; newSwitchChannels.splice(fromIndex, 1); newSwitchChannels.splice(fromIndex + (moveUp ? -1 : 1), 0, channel); setSwitchChannels(newSwitchChannels); } const moveCommand = (channelIndex: number, fromIndex: number, moveUp: boolean) => { const newSwitchChannels = [...switchChannels]; const command = newSwitchChannels[channelIndex].commands[fromIndex]; newSwitchChannels[channelIndex].commands.splice(fromIndex, 1); newSwitchChannels[channelIndex].commands.splice(fromIndex + (moveUp ? -1 : 1), 0, command); setSwitchChannels(newSwitchChannels); } useEffect(() => { send("getKvmSwitchEnabled", {}, resp => { if ("error" in resp) { notifications.error(`Failed to get KVM switch state: ${resp.error.data || "Unknown error"}`); return } const enabled = Boolean(resp.result); setKvmSwitchEnabled(enabled); if (enabled) { send("getKvmSwitchChannels", {}, resp => { if ("error" in resp) { notifications.error(`Failed to get switch channels: ${resp.error.data || "Unknown error"}`); return } setSwitchChannels(resp.result as SwitchChannel[]); }) } else { setSwitchChannels([]); } }); }, [send]); const setKvmSwitchEnabledHandler = (enabled: boolean) => { send("setKvmSwitchEnabled", { enabled: Boolean(enabled) }, resp => { if ("error" in resp) { notifications.error( `Failed to set KVM switch state: ${resp.error.data || "Unknown error"}`, ); return; } if (enabled) { notifications.success(`Enabled KVM Switch integration`); } else { notifications.success(`Disabled KVM Switch integration`); } setKvmSwitchEnabled(enabled); }); }; const getErrorsForChannelCommand = useCallback((index: number, channelIndex: number) => { const channel = switchChannels[index]; const command = channel.commands[channelIndex]; let errors: string[] = []; // Check name const nameValue = channel.name.trim(); if (nameValue.length === 0) { errors.push("Name cannot be empty"); } // Check address if (command.protocol !== "http" && command.protocol !== "https") { const addressValue = command.address.trim(); // Check that it is in form host:port const addressParts = addressValue.split(":"); if (addressValue.length === 0) { errors.push("Address cannot be empty"); } else if (addressParts.length !== 2) { errors.push("Address must be in form host:port"); } else if (addressParts[0].length === 0) { errors.push("Address host cannot be empty"); } else if (addressParts[1].length === 0) { errors.push("Address port cannot be empty"); } else if (!/^\d+$/.test(addressParts[1])) { errors.push("Address port must be a number"); } } // Check that commands map to the format if (command.format === "hex") { const messages = command.commands.split(",").map(x => x.trim()); if (messages.length === 0) { errors.push("Commands cannot be empty"); } else if (messages.some(x => !/^\s*0x[0-9a-fA-F]+\s*$/.test(x))) { errors.push("Commands must be in hex format (0x123020392193)"); } } if (command.format === "base64") { const messages = command.commands.split(",").map(x => x.trim()); if (messages.length === 0) { errors.push("Commands cannot be empty"); } let anyBase64Failed = false; if ("atob" in window) { for (const message of messages) { try { atob(message); } catch (e) { anyBase64Failed = true; } } } if (anyBase64Failed) { errors.push("Commands must be in base64 format"); } } if (command.format === "http-raw") { // Check that main components of HTTP request are in the command field const commandValue = command.commands.trim(); if (commandValue.length === 0) { errors.push("HTTP request cannot be empty"); } else { // Check that Host header is present if (!commandValue.includes("Host:")) { errors.push("HTTP request must contain Host header"); } } } return errors; }, [switchChannels]); useEffect(() => { if (!kvmSwitchEnabled) { return } // Iterate over getErrorsForChannelCommand var anyErrorsFound = false; for (const index in switchChannels) { if (anyErrorsFound) { break; } for (const channelIndex in switchChannels[index].commands) { const errors = getErrorsForChannelCommand(Number(index), Number(channelIndex)); if (errors.length > 0) { anyErrorsFound = true; break; } } } if (anyErrorsFound) { return; } const payload = { config: { channels: switchChannels } }; send("setKvmSwitchChannels", payload, resp => { if ("error" in resp) { notifications.error( `Failed to set switch channels: ${resp.error.data || "Unknown error"}`, ); return; } }); }, [kvmSwitchEnabled, switchChannels, getErrorsForChannelCommand]); return (
setKvmSwitchEnabledHandler(e.target.checked)} /> {!!kvmSwitchEnabled && switchChannels.length > 0 && (
{switchChannels.map((option, index) => (
{switchChannels.length > 1 && (
updateSwitchChannelData(index, { ...option, name: e.target.value })} /> {option.commands.map((command, commandIndex) => (
{option.commands.length > 1 && (
updateSwitchChannelCommands(index, commandIndex, { ...command, address: e.target.value })} />
updateSwitchChannelCommands( index, commandIndex, { ...command, protocol: e.target.value as SwitchChannelCommands["protocol"], format: GetCompatibleCommands(e.target.value as SwitchChannelCommands["protocol"])[0] as SwitchChannelCommands["format"], commands: "" })} options={[ { label: "TCP", value: "tcp" }, { label: "UDP", value: "udp" }, { label: "HTTP", value: "http" }, { label: "HTTPS", value: "https" }, ]} /> updateSwitchChannelCommands( index, commandIndex, { ...command, format: e.target.value as SwitchChannelCommands["format"], commands: "" })} options={[ { label: "HEX", value: "hex" }, { label: "Base64", value: "base64" }, { label: "ASCII", value: "ascii" }, { label: "HTTP Raw", value: "http-raw" }, ].filter(f => GetCompatibleCommands(command.protocol).includes(f.value))} />