add: rcon implementation

This commit is contained in:
2026-02-12 18:59:15 -08:00
commit 7c1697660f
7 changed files with 436 additions and 0 deletions

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
# MC god
Minecraft (MC) god is a tool that connects to a Minecraft server's RCON port, listens for messages from users by tailing the log, and interacts with players. It has the ability to call some functions to control things like weather, day/night cycle, and difficulty.
## Layout
This repository uses the Go standard layout described at https://github.com/golang-standards/project-layout.
- internal/pkg
- rcon -- contains utilities for interacting with the RCON service
- cmd/mcgod -- contains the main function
### RCON
https://github.com/gorcon/rcon is used to connect to the RCON. The system expects the user provide a RCON_ADDRESS, and RCON_PASSWORD environment flag.
# Handling updates
Use `go mod tidy` to update go.mod; do not directly modify it.
# Best practices
## Only capture environment/flags in main
To make testing easier, all utility packages should accept arguments that they need. These arguments sometimes get provided by the user, via flags or environment variables. Such variables should generally only be captured in the top-level package (i.e., main), and not helpers.

92
cmd/mcgod/main.go Normal file
View File

@@ -0,0 +1,92 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"tipsy.codes/charles/mc-god/v2/internal/pkg/rcon"
)
func main() {
// Create a context that will be cancelled on interrupt signals
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received interrupt signal, shutting down...")
cancel()
}()
// Create RCON client from environment variables
log.Println("Connecting to Minecraft server via RCON...")
client, err := rcon.NewFromEnv()
if err != nil {
log.Fatalf("Failed to create RCON client: %v", err)
}
defer func() {
if err := client.Close(); err != nil {
log.Printf("Error closing RCON connection: %v", err)
}
}()
// Perform a health check
log.Println("Performing health check...")
if err := client.HealthCheck(); err != nil {
log.Fatalf("Health check failed: %v", err)
}
log.Println("Connected successfully!")
// Example usage of various RCON functions
log.Println("Executing sample commands...")
// Get server info
info, err := client.GetServerInfo()
if err != nil {
log.Printf("Failed to get server info: %v", err)
} else {
log.Printf("Server info: %s", info)
}
// Set weather to clear
if err := client.SetWeather("clear"); err != nil {
log.Printf("Failed to set weather: %v", err)
} else {
log.Println("Weather set to clear")
}
// Set time to day
if err := client.SetTime("day"); err != nil {
log.Printf("Failed to set time: %v", err)
} else {
log.Println("Time set to day")
}
// Set difficulty to normal
if err := client.SetDifficulty("normal"); err != nil {
log.Printf("Failed to set difficulty: %v", err)
} else {
log.Println("Difficulty set to normal")
}
// Example of executing a custom command
response, err := client.ExecuteWithTimeout("list", 5*time.Second)
if err != nil {
log.Printf("Failed to execute 'list' command: %v", err)
} else {
log.Printf("Players list response: %s", response)
}
// Keep the application running until interrupted
log.Println("MC God is now running. Press Ctrl+C to stop.")
<-ctx.Done()
log.Println("MC God stopped gracefully.")
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module tipsy.codes/charles/mc-god/v2
go 1.25.6
require github.com/gorcon/rcon v1.4.0

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=

View 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
View 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())
}
}

View 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")
}