kvm/remote_kvm.go

264 lines
7.4 KiB
Go

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, ",") {
// Parse HEX
b, err := hex.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 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
}