feat: Add WebSocket RCON client to companion agent
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
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>
This commit is contained in:
80
companion-agent/internal/rcon/client.go
Normal file
80
companion-agent/internal/rcon/client.go
Normal file
@@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user