mirror of https://github.com/jetkvm/kvm.git
265 lines
7.5 KiB
Go
265 lines
7.5 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, ",") {
|
|
// 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
|
|
}
|