Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type Config struct {
TLSMode string `json:"tls_mode"`
UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"`
// Remote KVM
RemoteKvmEnabled bool `json:"remote_kvm_enabled"`
RemoteKvmSelectedChannel string `json:"remote_kvm_selected_channel"`
RemoteKvmChannels []SwitchChannel `json:"remote_kvm_channels"`
}

const configPath = "/userdata/kvm_config.json"
Expand Down
66 changes: 66 additions & 0 deletions jsonrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,65 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
return *config.UsbDevices, nil
}

func rpcGetKvmSwitchEnabled() (bool, error) {
return config.RemoteKvmEnabled, nil
}

func rpcSetKvmSwitchEnabled(enabled bool) error {
config.RemoteKvmEnabled = enabled
config.RemoteKvmSelectedChannel = ""
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}

func rpcGetKvmSwitchSelectedChannel() (*SwitchChannel, error) {
if !config.RemoteKvmEnabled {
return nil, fmt.Errorf("KVM switch is disabled")
}
if config.RemoteKvmSelectedChannel == "" {
return nil, fmt.Errorf("no channel selected")
}

for _, c := range config.RemoteKvmChannels {
if c.Id == config.RemoteKvmSelectedChannel {
return &c, nil
}
}

return nil, fmt.Errorf("channel not found")
}

func rpcSetKvmSwitchSelectedChannel(id string) error {
// Check that the channel is known (exists in the config)
err := RemoteKvmSwitchChannel(id)
if err != nil {
return fmt.Errorf("unable to select channel by ID %s: %w", id, err)
}

config.RemoteKvmSelectedChannel = id
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}

func rpcGetKvmSwitchChannels() ([]SwitchChannel, error) {
if !config.RemoteKvmEnabled {
return nil, fmt.Errorf("KVM switch is disabled")
}
return config.RemoteKvmChannels, nil
}

func rpcSetKvmSwitchChannels(newConfig SwitchChannelConfig) error {
config.RemoteKvmChannels = newConfig.Channels
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
return nil
}

func updateUsbRelatedConfig() error {
if err := gadget.UpdateGadgetConfig(); err != nil {
return fmt.Errorf("failed to write gadget config: %w", err)
Expand Down Expand Up @@ -857,4 +916,11 @@ var rpcHandlers = map[string]RPCHandler{
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
// remote KVM
"getKvmSwitchEnabled": {Func: rpcGetKvmSwitchEnabled},
"setKvmSwitchEnabled": {Func: rpcSetKvmSwitchEnabled, Params: []string{"enabled"}},
"getKvmSwitchChannels": {Func: rpcGetKvmSwitchChannels},
"setKvmSwitchChannels": {Func: rpcSetKvmSwitchChannels, Params: []string{"config"}},
"getKvmSwitchSelectedChannel": {Func: rpcGetKvmSwitchSelectedChannel},
"setKvmSwitchSelectedChannel": {Func: rpcSetKvmSwitchSelectedChannel, Params: []string{"id"}},
}
264 changes: 264 additions & 0 deletions remote_kvm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package kvm

import (
"bufio"
"bytes"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net"
"net/http"
"strings"
)

// SwitchChannelCommandProtocol is the protocol used to connect to the remote KVM switch
type SwitchChannelCommandProtocol string

// SwitchChannelCommandFormat is the format of the commands (hex, base64, ascii)
type SwitchChannelCommandFormat string

const (
SwitchChannelCommandProtocolTCP SwitchChannelCommandProtocol = "tcp"
SwitchChannelCommandProtocolUDP SwitchChannelCommandProtocol = "udp"
SwitchChannelCommandProtocolHTTP SwitchChannelCommandProtocol = "http"
SwitchChannelCommandProtocolHTTPs SwitchChannelCommandProtocol = "https"
)

const (
SwitchChannelCommandFormatHEX SwitchChannelCommandFormat = "hex"
SwitchChannelCommandFormatBase64 SwitchChannelCommandFormat = "base64"
SwitchChannelCommandFormatASCII SwitchChannelCommandFormat = "ascii"
SwitchChannelCommandFormatHTTP SwitchChannelCommandFormat = "http-raw"
)

// SwitchChannelCommand represents a command to be sent to a remote KVM switch
type SwitchChannelCommand struct {
Address string `json:"address"`
Protocol SwitchChannelCommandProtocol `json:"protocol"`
Format SwitchChannelCommandFormat `json:"format"`
Commands string `json:"commands"`
}

// SwitchChannel represents a remote KVM switch channel
type SwitchChannel struct {
Commands []SwitchChannelCommand `json:"commands"`
Name string `json:"name"`
Id string `json:"id"`
}

// SwitchChannelConfig represents the remote KVM switch configuration
type SwitchChannelConfig struct {
Channels []SwitchChannel `json:"channels"`
}

func remoteKvmSwitchChannelRawIP(channel *SwitchChannel, idx int, command *SwitchChannelCommand) error {
var err error
var payloadBytes = make([][]byte, 0)

// Parse commands
switch command.Format {
case SwitchChannelCommandFormatHEX:
// Split by comma and parse as HEX
for _, cmd := range strings.Split(command.Commands, ",") {
// Trim spaces, remove 0x prefix and parse as HEX
commandText := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(cmd), "0x"))
b, err := hex.DecodeString(commandText)
if err != nil {
return fmt.Errorf("invalid command provided for command #%d: %w", idx, err)
}
payloadBytes = append(payloadBytes, b)
}
break
case SwitchChannelCommandFormatBase64:
// Split by comma and parse as Base64
for _, cmd := range strings.Split(command.Commands, ",") {
// Parse Base64
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(cmd))
if err != nil {
return fmt.Errorf("invalid command provided for command #%d: %w", idx, err)
}
payloadBytes = append(payloadBytes, b)
}
break
case SwitchChannelCommandFormatASCII:
// Split by newline and parse as ASCII
for _, cmd := range strings.Split(command.Commands, "\n") {
// Parse ASCII
b := []byte(strings.TrimSpace(cmd))
payloadBytes = append(payloadBytes, b)
}
break
default:
return fmt.Errorf("invalid format provided for %s command #%d: %s", command.Protocol, idx, command.Format)
}

// Connect to the address
var conn net.Conn
switch command.Protocol {
case SwitchChannelCommandProtocolTCP:
conn, err = net.Dial("tcp", command.Address)
break
case SwitchChannelCommandProtocolUDP:
conn, err = net.Dial("udp", command.Address)
break
default:
return fmt.Errorf("invalid protocol provided for command #%d: %s", idx, command.Protocol)
}

if err != nil {
return fmt.Errorf("failed to connect to address for command #%d: %w", idx, err)
}
if conn == nil {
return fmt.Errorf("failed to connect to address for command #%d: connection is nil", idx)
}

defer func() {
if conn != nil {
_ = conn.Close()
}
}()

// Send commands
for _, b := range payloadBytes {
_, err := conn.Write(b)
if err != nil {
return fmt.Errorf("failed to send command for command #%d: %w", idx, err)
}
}

// Close the connection
err = conn.Close()
if err != nil {
return fmt.Errorf("failed to close connection for command #%d: %w", idx, err)
}

return nil
}

func remoteKvmSwitchChannelHttps(channel *SwitchChannel, idx int, command *SwitchChannelCommand) error {
var err error

// Validation
scheme := string(command.Protocol)
if scheme != "http" && scheme != "https" {
return fmt.Errorf("invalid protocol provided for command #%d: %s", idx, command.Protocol)
}

if command.Format != SwitchChannelCommandFormatHTTP {
return fmt.Errorf("invalid format provided for %s command #%d: %s", command.Protocol, idx, command.Format)
}

httpPayload := command.Commands
// If there is no \r\n at then end - add
if !strings.HasSuffix(httpPayload, "\r\n\r\n") {
if strings.HasSuffix(httpPayload, "\r\n") {
httpPayload += "\r\n"
} else {
httpPayload += "\r\n\r\n"
}
}

// Parse request
requestReader := bufio.NewReader(strings.NewReader(httpPayload))
r, err := http.ReadRequest(requestReader)
if err != nil {
return fmt.Errorf("failed to read request for command #%d: %w", idx, err)
}
r.RequestURI, r.URL.Scheme, r.URL.Host = "", scheme, r.Host

// Execute request
resp, err := http.DefaultClient.Do(r)
if err != nil {
return fmt.Errorf("failed to send request for command #%d: %w", idx, err)
}

// Read data to buffer
var buf bytes.Buffer
_, err = io.Copy(&buf, resp.Body)
if err != nil {
return fmt.Errorf("failed to read response for command #%d: %w", idx, err)
}

// Close the response
defer func() {
if resp != nil {
_ = resp.Body.Close()
}
}()

if resp.StatusCode >= 400 || resp.StatusCode < 200 {
if buf.Len() > 0 {
return fmt.Errorf("failed to send request for command #%d: %s: %s", idx, resp.Status, buf.String())
} else {
return fmt.Errorf("failed to send request for command #%d: %s", idx, resp.Status)
}
}

return nil
}

// RemoteKvmSwitchChannel sends commands to a remote KVM switch
func RemoteKvmSwitchChannel(id string) error {
if !config.RemoteKvmEnabled {
return fmt.Errorf("remote KVM is not enabled")
}
if len(config.RemoteKvmChannels) == 0 {
return fmt.Errorf("no remote KVM channels configured")
}
if len(id) == 0 {
return fmt.Errorf("no channel id provided")
}

var channel *SwitchChannel

for _, c := range config.RemoteKvmChannels {
if c.Id == id {
channel = &c
break
}
}
if channel == nil {
return fmt.Errorf("channel not found")
}

// Try to run commands
if len(channel.Commands) == 0 {
return fmt.Errorf("no commands found for channel %s", id)
}

for idx, c := range channel.Commands {
// Initial validation
if c.Protocol == SwitchChannelCommandProtocolTCP || c.Protocol == SwitchChannelCommandProtocolUDP {
if c.Address == "" {
return fmt.Errorf("no address provided for command #%d", idx)
}

_, _, err := net.SplitHostPort(c.Address)
if err != nil {
return fmt.Errorf("invalid address provided for command #%d: %w", idx, err)
}
}

if c.Protocol == "" {
return fmt.Errorf("no protocol provided for command #%d", idx)
}
if c.Format == "" {
return fmt.Errorf("no format provided for command #%d", idx)
}
if c.Commands == "" {
return fmt.Errorf("no commands provided for command #%d", idx)
}

switch {
case c.Protocol == SwitchChannelCommandProtocolTCP || c.Protocol == SwitchChannelCommandProtocolUDP:
return remoteKvmSwitchChannelRawIP(channel, idx, &c)
case c.Protocol == SwitchChannelCommandProtocolHTTPs || c.Protocol == SwitchChannelCommandProtocolHTTP:
return remoteKvmSwitchChannelHttps(channel, idx, &c)
default:
return fmt.Errorf("invalid protocol provided for command #%d: %s", idx, c.Protocol)
}
}

return nil
}
13 changes: 13 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"recharts": "^2.15.0",
"tailwind-merge": "^2.5.5",
"usehooks-ts": "^3.1.0",
"uuid": "^11.1.0",
"validator": "^13.12.0",
"xterm": "^5.3.0",
"zustand": "^4.5.2"
Expand Down
Loading