224 lines
4.9 KiB
Go
224 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/hamburghammer/rcon"
|
|
flags "github.com/spf13/pflag"
|
|
)
|
|
|
|
const (
|
|
defaultHost = "localhost"
|
|
defaultPort = "25575"
|
|
defaultPassword = ""
|
|
|
|
envVarPrefix = "RCON_CLI_"
|
|
)
|
|
|
|
// resetColor is the ASCII code for resetting the color.
|
|
const resetColor = "\u001B[0m"
|
|
|
|
// colors a map with the ASCII color codes for Unix terms.
|
|
var colors = map[string]string{
|
|
"0": "\u001B[30m", // black
|
|
"1": "\u001B[34m", // dark blue
|
|
"2": "\u001B[32m", // dark green
|
|
"3": "\u001B[36m", // dark aqua
|
|
"4": "\u001B[31m", // dark red
|
|
"5": "\u001B[35m", // dark purple
|
|
"6": "\u001B[33m", // gold
|
|
"7": "\u001B[37m", // gray
|
|
"8": "\u001B[30m", // dark gray
|
|
"9": "\u001B[34m", // blue
|
|
"a": "\u001B[32m", // green
|
|
"b": "\u001B[32m", // aqua
|
|
"c": "\u001B[31m", // red
|
|
"d": "\u001B[35m", // light purple
|
|
"e": "\u001B[33m", // yellow
|
|
"f": "\u001B[37m", // white
|
|
"k": "", // random
|
|
"m": "\u001B[9m", // strikethrough
|
|
"o": "\u001B[3m", // italic
|
|
"l": "\u001B[1m", // bold
|
|
"n": "\u001B[4m", // underline
|
|
"r": resetColor, // reset
|
|
}
|
|
|
|
// vars holding the parsed configuration
|
|
var (
|
|
host string
|
|
port string
|
|
password string
|
|
help bool
|
|
)
|
|
|
|
func main() {
|
|
commands, _ := parseArgs(os.Args[1:])
|
|
loadEnvIfNotSet()
|
|
if help {
|
|
printHelp()
|
|
os.Exit(0)
|
|
return
|
|
}
|
|
|
|
err := run(strings.Join(commands, " "))
|
|
if err != nil {
|
|
_, err = fmt.Fprintln(os.Stderr, err.Error())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func parseArgs(args []string) ([]string, error) {
|
|
flags.StringVar(&host, "host", defaultHost, "RCON server's hostname.")
|
|
flags.StringVar(&port, "port", defaultPort, "RCON server's port.")
|
|
flags.StringVar(&password, "password", defaultPassword, "RCON server's password.")
|
|
flags.BoolVarP(&help, "help", "h", false, "Prints this help message and exits.")
|
|
|
|
err := flags.CommandLine.Parse(args)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
command := flags.Args()
|
|
|
|
return command, nil
|
|
}
|
|
|
|
func loadEnvIfNotSet() {
|
|
envHost := os.Getenv(fmt.Sprintf("%sHOST", envVarPrefix))
|
|
if envHost != "" && host == defaultHost {
|
|
host = envHost
|
|
}
|
|
|
|
envPort := os.Getenv(fmt.Sprintf("%sPORT", envVarPrefix))
|
|
if envPort != "" && port == defaultPort {
|
|
port = envPort
|
|
}
|
|
|
|
envPassword := os.Getenv(fmt.Sprintf("%sPASSWORD", envVarPrefix))
|
|
if envPassword != "" && password == defaultPassword {
|
|
password = envPassword
|
|
}
|
|
}
|
|
|
|
func run(cmd string) error {
|
|
hostPort := net.JoinHostPort(host, port)
|
|
remoteConsole, err := rcon.Dial(hostPort, password)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to RCON server: %w", err)
|
|
}
|
|
defer remoteConsole.Close()
|
|
|
|
if len(cmd) == 0 {
|
|
err = executeInteractive(remoteConsole)
|
|
} else {
|
|
resp, err := execute(cmd, remoteConsole)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Println(resp)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func printHelp() {
|
|
_, _ = fmt.Println(`rcon-cli is a CLI to interact with a RCON server.
|
|
It can be run in an interactive mode or to execute a single command.
|
|
|
|
USAGE:
|
|
rcon-cli [FLAGS] [RCON command ...]
|
|
|
|
FLAGS:`)
|
|
flags.PrintDefaults()
|
|
fmt.Printf(`
|
|
ENVIRONMENT VARIABLE:
|
|
All flags can be set through the flag name in capslock with the %s prefix (see examples).
|
|
Flags have allways priority over env vars!
|
|
|
|
EXAMPLES:
|
|
rcon-cli --host 127.0.0.1 --port 25575
|
|
rcon-cli --password admin123 stop
|
|
%sPORT=25575 rcon-cli stop
|
|
`, envVarPrefix, envVarPrefix)
|
|
}
|
|
|
|
func executeInteractive(remoteConsole *rcon.RemoteConsole) error {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
_, _ = fmt.Println("To quit the session type 'exit'.")
|
|
_, _ = fmt.Print("> ")
|
|
for scanner.Scan() {
|
|
cmd := scanner.Text()
|
|
if cmd == "exit" {
|
|
return nil
|
|
}
|
|
resp, err := execute(cmd, remoteConsole)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _ = fmt.Println(resp)
|
|
_, _ = fmt.Print("> ")
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return fmt.Errorf("reading standard input: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func execute(cmd string, remoteConsole *rcon.RemoteConsole) (string, error) {
|
|
resp := ""
|
|
reqID, err := remoteConsole.Write(cmd)
|
|
if err != nil {
|
|
return resp, fmt.Errorf("failed to send command: %w", err)
|
|
}
|
|
|
|
resp, respID, err := remoteConsole.Read()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return resp, nil
|
|
}
|
|
return resp, fmt.Errorf("failed to read command: %w", err)
|
|
}
|
|
|
|
if reqID != respID {
|
|
return resp, errors.New("the response id didn't match the request id")
|
|
}
|
|
|
|
resp = colorize(resp)
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// colorize tries to add the color codes for the terminal.
|
|
// works on none windows machines.
|
|
func colorize(str string) string {
|
|
const sectionSign = "§"
|
|
|
|
for code := range colors {
|
|
if runtime.GOOS == "windows" {
|
|
str = strings.ReplaceAll(str, sectionSign+code, "")
|
|
} else {
|
|
str = strings.ReplaceAll(str, sectionSign+code, colors[code])
|
|
}
|
|
}
|
|
// reset color after each new line
|
|
if runtime.GOOS != "windows" {
|
|
return strings.ReplaceAll(str, "\n", "\n"+resetColor)
|
|
}
|
|
|
|
return str
|
|
}
|