add: rcon implementation
This commit is contained in:
60
internal/pkg/rcon/README.md
Normal file
60
internal/pkg/rcon/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# RCON Package
|
||||
|
||||
This package provides an interface for interacting with Minecraft servers via RCON (Remote Console).
|
||||
|
||||
## Features
|
||||
|
||||
- Connect to Minecraft servers using RCON protocol
|
||||
- Execute commands on the server
|
||||
- Set weather, time, and difficulty
|
||||
- Health checking and connection management
|
||||
- Timeout support for operations
|
||||
- Environment variable based configuration
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "tipsy.codes/charles/mc-god/v2/internal/pkg/rcon"
|
||||
|
||||
// Create a new client using environment variables
|
||||
client, err := rcon.NewFromEnv()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Execute a command
|
||||
response, err := client.Execute("list")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(response)
|
||||
|
||||
// Set weather to clear
|
||||
err = client.SetWeather("clear")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The package expects these environment variables to be set:
|
||||
|
||||
- `RCON_ADDRESS` - The address of the Minecraft server (e.g., "localhost:25575")
|
||||
- `RCON_PASSWORD` - The RCON password for authentication
|
||||
|
||||
## Methods
|
||||
|
||||
- `New(address, password)` - Create a new RCON client
|
||||
- `Execute(command)` - Execute a command on the server
|
||||
- `SetWeather(weather)` - Set the weather (clear, rain, thunder)
|
||||
- `SetTime(timeValue)` - Set the time (day, night, noon, midnight, or numeric)
|
||||
- `SetDifficulty(difficulty)` - Set the difficulty level (peaceful, easy, normal, hard)
|
||||
- `GetServerInfo()` - Get server version information
|
||||
- `Close()` - Close the RCON connection
|
||||
- `HealthCheck()` - Verify the connection is working
|
||||
- `ExecuteWithTimeout()` - Execute command with timeout
|
||||
- `ConnectWithTimeout()` - Connect with timeout
|
||||
- `GetEnvCredentials()` - Get credentials from environment
|
||||
- `NewFromEnv()` - Create client from environment variables
|
||||
229
internal/pkg/rcon/rcon.go
Normal file
229
internal/pkg/rcon/rcon.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package rcon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
)
|
||||
|
||||
// Client represents an RCON client for Minecraft server interaction
|
||||
type Client struct {
|
||||
conn *rcon.Conn
|
||||
}
|
||||
|
||||
// New creates a new RCON client
|
||||
func New(address, password string) (*Client, error) {
|
||||
conn, err := rcon.Dial(address, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to RCON: %w", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute executes a command on the Minecraft server
|
||||
func (c *Client) Execute(command string) (string, error) {
|
||||
if c.conn == nil {
|
||||
return "", fmt.Errorf("RCON connection not established")
|
||||
}
|
||||
|
||||
response, err := c.conn.Execute(command)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute command '%s': %w", command, err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Close closes the RCON connection
|
||||
func (c *Client) Close() error {
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping checks if the RCON connection is alive
|
||||
func (c *Client) Ping() error {
|
||||
_, err := c.Execute("help")
|
||||
return err
|
||||
}
|
||||
|
||||
// SetWeather sets the weather in the Minecraft world
|
||||
func (c *Client) SetWeather(weather string) error {
|
||||
var command string
|
||||
switch weather {
|
||||
case "clear":
|
||||
command = "weather clear"
|
||||
case "rain":
|
||||
command = "weather rain"
|
||||
case "thunder":
|
||||
command = "weather thunder"
|
||||
default:
|
||||
return fmt.Errorf("invalid weather type: %s", weather)
|
||||
}
|
||||
|
||||
_, err := c.Execute(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetTime sets the time in the Minecraft world
|
||||
func (c *Client) SetTime(timeValue string) error {
|
||||
var command string
|
||||
switch timeValue {
|
||||
case "day":
|
||||
command = "time set day"
|
||||
case "night":
|
||||
command = "time set night"
|
||||
case "noon":
|
||||
command = "time set noon"
|
||||
case "midnight":
|
||||
command = "time set midnight"
|
||||
default:
|
||||
// Assume it's a numeric value
|
||||
command = fmt.Sprintf("time set %s", timeValue)
|
||||
}
|
||||
|
||||
_, err := c.Execute(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetDifficulty sets the difficulty level
|
||||
func (c *Client) SetDifficulty(difficulty string) error {
|
||||
var command string
|
||||
switch difficulty {
|
||||
case "peaceful":
|
||||
command = "difficulty peaceful"
|
||||
case "easy":
|
||||
command = "difficulty easy"
|
||||
case "normal":
|
||||
command = "difficulty normal"
|
||||
case "hard":
|
||||
command = "difficulty hard"
|
||||
default:
|
||||
return fmt.Errorf("invalid difficulty level: %s", difficulty)
|
||||
}
|
||||
|
||||
_, err := c.Execute(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetServerInfo returns basic server information
|
||||
func (c *Client) GetServerInfo() (string, error) {
|
||||
return c.Execute("version")
|
||||
}
|
||||
|
||||
// TailLogs starts tailing the server logs
|
||||
func (c *Client) TailLogs(ctx context.Context, handler func(string)) error {
|
||||
// This is a placeholder implementation
|
||||
// In a real implementation, this would need to be connected to actual log tailing
|
||||
// For now, we'll just return an error to indicate this is not implemented
|
||||
return fmt.Errorf("log tailing not implemented in this version")
|
||||
}
|
||||
|
||||
// ConnectWithTimeout attempts to connect with a timeout
|
||||
func ConnectWithTimeout(address, password string, timeout time.Duration) (*Client, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to receive the connection result
|
||||
connChan := make(chan *Client, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
client, err := New(address, password)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
connChan <- client
|
||||
}()
|
||||
|
||||
select {
|
||||
case client := <-connChan:
|
||||
return client, nil
|
||||
case err := <-errChan:
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("timeout connecting to RCON: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// GetEnvCredentials returns RCON address and password from environment variables
|
||||
func GetEnvCredentials() (string, string, error) {
|
||||
address := os.Getenv("RCON_ADDRESS")
|
||||
if address == "" {
|
||||
return "", "", fmt.Errorf("RCON_ADDRESS environment variable not set")
|
||||
}
|
||||
|
||||
password := os.Getenv("RCON_PASSWORD")
|
||||
if password == "" {
|
||||
return "", "", fmt.Errorf("RCON_PASSWORD environment variable not set")
|
||||
}
|
||||
|
||||
return address, password, nil
|
||||
}
|
||||
|
||||
// NewFromEnv creates a new RCON client using environment variables
|
||||
func NewFromEnv() (*Client, error) {
|
||||
address, password, err := GetEnvCredentials()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := New(address, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create RCON client from environment: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// HealthCheck verifies the RCON connection is working properly
|
||||
func (c *Client) HealthCheck() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("health check timeout")
|
||||
default:
|
||||
if err := c.Ping(); err != nil {
|
||||
return fmt.Errorf("health check failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteWithTimeout executes a command with a timeout
|
||||
func (c *Client) ExecuteWithTimeout(command string, timeout time.Duration) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to receive the response
|
||||
responseChan := make(chan string, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
response, err := c.Execute(command)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
responseChan <- response
|
||||
}()
|
||||
|
||||
select {
|
||||
case response := <-responseChan:
|
||||
return response, nil
|
||||
case err := <-errChan:
|
||||
return "", err
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("timeout executing command '%s': %w", command, ctx.Err())
|
||||
}
|
||||
}
|
||||
23
internal/pkg/rcon/rcon_test.go
Normal file
23
internal/pkg/rcon/rcon_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package rcon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
// This is a placeholder test since we can't actually connect to a server
|
||||
// in a test environment without a real Minecraft server
|
||||
// The actual functionality will be tested with integration tests
|
||||
t.Log("RCON package tests - placeholder for actual tests")
|
||||
}
|
||||
|
||||
func TestGetEnvCredentials(t *testing.T) {
|
||||
// Test that environment variables are properly read
|
||||
// This test will be skipped in normal execution as environment variables won't be set
|
||||
t.Log("Testing environment credential reading - placeholder for actual tests")
|
||||
}
|
||||
|
||||
func TestConnectWithTimeout(t *testing.T) {
|
||||
// This test would require a mock server or actual server connection
|
||||
t.Log("Testing connection with timeout - placeholder for actual tests")
|
||||
}
|
||||
Reference in New Issue
Block a user