Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 6 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,13 @@
# tcprcon

- [tcprcon](#tcprcon)
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Interactive Mode](#interactive-mode)
- [Single Command Mode](#single-command-mode)
- [Using Environment Variable for Password](#using-environment-variable-for-password)
- [CLI Flags](#cli-flags)
- [Using as a Library](#using-as-a-library)
- [Streaming Responses](#streaming-responses)
- [tcprcon-cli](#tcprcon-cli)
- [License](#license)


A fully native RCON client implementation, zero third parties*

<sub>*except for other golang maintained packages about terminal emulators, until i fully master tty :(</sub>

![tcprcon demo](.meta/demo.png)

## Features

- **Interactive Terminal UI**: full-screen exclusive TUI (like vim or nano)
- **Single Command Mode**: execute a single RCON command and exit
- **Multiple Authentication Methods**: supports password via CLI flag, environment variable (`rcon_password`), or secure prompt
- **Configurable Logging**: syslog-style severity levels for debugging
- **Installable as library**: use the RCON client in your own Go projects, ([see examples](#using-as-a-library))

## Installation

```bash
go install github.com/UltimateForm/tcprcon@latest
```

Or build from source:

<sub>note: requires golang 1.22+</sub>

```bash
git clone https://github.com/UltimateForm/tcprcon.git
cd tcprcon
go build -o tcprcon .
```

## Usage

### Interactive Mode

```bash
tcprcon --address=192.168.1.100 --port=7778
```

### Single Command Mode

```bash
tcprcon --address=192.168.1.100 --cmd="playerlist"
```

### Using Environment Variable for Password

```bash
export rcon_password="your_password"
tcprcon --address=192.168.1.100
```

## CLI Flags

```
-address string
RCON address, excluding port (default "localhost")
-cmd string
command to execute, if provided will not enter into interactive mode
-log uint
sets log level (syslog severity tiers) for execution (default 4)
-port uint
RCON port (default 7778)
-pw string
RCON password, if not provided will attempt to load from env variables, if unavailable will prompt
```
A fully native RCON client implementation, zero deps

## Using as a Library

Expand Down Expand Up @@ -163,7 +93,10 @@ func main() {
}
```

## tcprcon-cli

https://github.com/UltimateForm/tcprcon-cli

## License

This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)**. See [LICENSE](LICENSE) for details.

95 changes: 29 additions & 66 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,134 +2,97 @@ package cmd

import (
"bufio"
"context"
"errors"
"flag"
"fmt"
"os"
"strconv"
"strings"

"github.com/UltimateForm/tcprcon/internal/ansi"
"github.com/UltimateForm/tcprcon/pkg/logger"
"github.com/UltimateForm/tcprcon/pkg/common_rcon"
"github.com/UltimateForm/tcprcon/pkg/logger"
"github.com/UltimateForm/tcprcon/pkg/packet"
"github.com/UltimateForm/tcprcon/pkg/rcon"
"golang.org/x/term"
)

var addressParam string
var portParam uint
var passwordParam string
var logLevelParam uint
var inputCmdParam string

func init() {
flag.StringVar(&addressParam, "address", "localhost", "RCON address, excluding port")
flag.UintVar(&portParam, "port", 7778, "RCON port")
flag.StringVar(&passwordParam, "pw", "", "RCON password, if not provided will attempt to load from env variables, if unavailable will prompt")
flag.UintVar(&logLevelParam, "log", logger.LevelWarning, "sets log level (syslog serverity tiers) for execution")
flag.StringVar(&inputCmdParam, "cmd", "", "command to execute, if provided will not enter into interactive mode")
}

func determinePassword() (string, error) {
if len(passwordParam) > 0 {
logger.Debug.Println("using password from parameter")
return passwordParam, nil
}
envPassword := os.Getenv("rcon_password")
var password string
if len(envPassword) > 0 {
logger.Debug.Println("using password from os env")
r := ""
for r == "" {
fmt.Print("RCON password found in environment variables, use for authentication? (y/n) ")
stdinread := bufio.NewReader(os.Stdin)
stdinbytes, _isPrefix, err := stdinread.ReadLine()
stdinbytes, _, err := stdinread.ReadLine()
if err != nil {
return "", err
}
if _isPrefix {
logger.Err.Println("prefix not supported")
continue
}
r = string(stdinbytes)
}
if strings.ToLower(r) == "y" {
password = envPassword
}
}
if len(password) == 0 {
fmt.Print("RCON PASSWORD: ")
stdinbytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return "", err
if strings.ToLower(r) != "y" {
return "", errors.New("Unimplemented")
}
password = string(stdinbytes)
password = envPassword
}
return password, nil
}

func execInputCmd(rcon *rcon.Client) error {
logger.Debug.Println("executing input command: " + inputCmdParam)
execPacket := packet.New(rcon.Id(), packet.SERVERDATA_EXECCOMMAND, []byte(inputCmdParam))
fmt.Printf(
"(%v): SND CMD %v\n",
ansi.Format(strconv.Itoa(int(rcon.Id())), ansi.Green, ansi.Bold),
ansi.Format(inputCmdParam, ansi.Blue),
)
rcon.Write(execPacket.Serialize())
packetRes, err := packet.Read(rcon)
if err != nil {
return errors.Join(errors.New("error while reading from RCON client"), err)
}
fmt.Printf(
"(%v): RCV PKT %v\n%v\n",
ansi.Format(strconv.Itoa(int(rcon.Id())), ansi.Green, ansi.Bold),
ansi.Format(strconv.Itoa(int(packetRes.Type)), ansi.Green, ansi.Bold),
ansi.Format(strings.TrimRight(packetRes.BodyStr(), "\n\r"), ansi.Green),
)
return nil
}

func Execute() {
flag.Parse()
logLevel := uint8(logLevelParam)
logger.Setup(logLevel)
logger.Debug.Printf("parsed parameters: address=%v, port=%v, pw=%v, log=%v, cmd=%v\n", addressParam, portParam, passwordParam != "", logLevelParam, inputCmdParam)
logger.Setup(uint8(logLevelParam))
fullAddress := addressParam + ":" + strconv.Itoa(int(portParam))
shell := fmt.Sprintf("[rcon@%v]", fullAddress)
password, err := determinePassword()
if err != nil {
logger.Critical.Fatal(err)
}
logger.Debug.Printf("Dialing %v at port %v\n", addressParam, portParam)
rconClient, err := rcon.New(fullAddress)
rcon, err := rcon.New(fullAddress)
if err != nil {
logger.Critical.Fatal(err)
}
defer rconClient.Close()
defer rcon.Close()

logger.Debug.Println("Building auth packet")
auhSuccess, authErr := common_rcon.Authenticate(rconClient, password)
auhSuccess, authErr := common_rcon.Authenticate(rcon, password)
if authErr != nil {
logger.Critical.Println(errors.Join(errors.New("auth failure"), authErr))
return
logger.Err.Fatal(err)
}
if !auhSuccess {
logger.Critical.Println(errors.New("unknown auth error"))
return
logger.Err.Fatal(errors.New("auth failure"))
}

if inputCmdParam != "" {
if err := execInputCmd(rconClient); err != nil {
logger.Critical.Println(err)
for {
logger.Info.Println("-----STARTING CMD EXCHANGE-----")
stdinread := bufio.NewReader(os.Stdin)
fmt.Printf("%v#", shell)
cmd, _, err := stdinread.ReadLine()
if err != nil {
logger.Critical.Fatal(err)
}
return
} else {
// could just rely on early return but i feel anxious :D
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runRconTerminal(rconClient, ctx, logLevel)
currId := rcon.Id()
execPacket := packet.New(currId, packet.SERVERDATA_EXECCOMMAND, cmd)
rcon.Write(execPacket.Serialize())
logger.Debug.Println("Reading from server...")
responsePkt, err := packet.Read(rcon)
if err != nil {
logger.Critical.Fatal(errors.Join(errors.New("error while reading from RCON client"), err))
}
fmt.Printf("OUT: %v\n", responsePkt.BodyStr())
}

}
105 changes: 0 additions & 105 deletions cmd/terminal.go

This file was deleted.

4 changes: 0 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
module github.com/UltimateForm/tcprcon

go 1.25.3

require golang.org/x/sys v0.39.0

require golang.org/x/term v0.38.0
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
Empty file removed internal/.gitkeep
Empty file.
Loading