Files
corrosion-admin-panel/companion-agent/internal/rcon/client.go
Vantz Stockwell eb57c51a24
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
feat: Add WebSocket RCON client to companion agent
Wire gorilla/websocket into the Go companion agent to send arbitrary
console commands (e.g. oxide.reload BetterLoot) to the Rust Dedicated
Server's WebRCON endpoint. Adds RCON_PORT and RCON_PASSWORD env vars,
a new "command" action on the existing cmd.server NATS subject, and
the internal/rcon package that handles the JSON-over-WebSocket protocol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:16:47 -05:00

81 lines
1.9 KiB
Go

package rcon
import (
"encoding/json"
"fmt"
"net/url"
"time"
"github.com/gorilla/websocket"
)
// RconRequest is the JSON payload sent to Rust's WebRCON.
type RconRequest struct {
Identifier int `json:"Identifier"`
Message string `json:"Message"`
Name string `json:"Name"`
}
// RconResponse is the JSON payload received from Rust's WebRCON.
type RconResponse struct {
Identifier int `json:"Identifier"`
Message string `json:"Message"`
Type string `json:"Type"`
}
// SendCommand opens a WebSocket to the Rust server's RCON port, sends
// a single command, reads the response, and closes the connection.
func SendCommand(port int, password string, command string) (string, error) {
u := url.URL{
Scheme: "ws",
Host: fmt.Sprintf("127.0.0.1:%d", port),
Path: fmt.Sprintf("/%s", password),
}
dialer := websocket.Dialer{
HandshakeTimeout: 5 * time.Second,
}
conn, _, err := dialer.Dial(u.String(), nil)
if err != nil {
return "", fmt.Errorf("rcon dial failed: %w", err)
}
defer conn.Close()
// Set read deadline
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
req := RconRequest{
Identifier: 1,
Message: command,
Name: "Corrosion",
}
data, err := json.Marshal(req)
if err != nil {
return "", fmt.Errorf("rcon marshal failed: %w", err)
}
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
return "", fmt.Errorf("rcon write failed: %w", err)
}
// Read response — may get multiple messages (Generic, Warning, etc.)
// We want the first response with our Identifier.
for {
_, message, err := conn.ReadMessage()
if err != nil {
return "", fmt.Errorf("rcon read failed: %w", err)
}
var resp RconResponse
if err := json.Unmarshal(message, &resp); err != nil {
continue // skip unparseable messages
}
if resp.Identifier == req.Identifier {
return resp.Message, nil
}
}
}