mirror of
https://github.com/onyx-and-iris/q3rcon.git
synced 2026-03-02 17:09:19 +00:00
Compare commits
10 Commits
a55de6fe50
...
51e8ac85be
| Author | SHA1 | Date | |
|---|---|---|---|
| 51e8ac85be | |||
| 79d53f34da | |||
| 7c5a3523bf | |||
| 44a528e31d | |||
| 48b23321e5 | |||
| d05ed91473 | |||
| 5c28c4e8b7 | |||
| 53f30981fd | |||
| fce6fa43fc | |||
| 0b9546ee0e |
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.envrc
|
||||
|
||||
# Added by goreleaser init:
|
||||
dist/
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@ -11,7 +11,19 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
# [0.4.0] - 2026-02-15
|
||||
# [0.5.0] - 2026-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- CLI configuration can be managed through env vars, see [Environment Variables](https://github.com/onyx-and-iris/q3rcon?tab=readme-ov-file#environment-variables) under Configuration in README.
|
||||
|
||||
### Changed
|
||||
|
||||
- The CLI now supports `--long` and `-short` style flags. Several examples in README.
|
||||
- `--help` output has been improved.
|
||||
|
||||
|
||||
# [0.4.1] - 2026-02-15
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
58
README.md
58
README.md
@ -17,9 +17,15 @@ Quake3 Rcon works by firing UDP packets to the game server port, responses may b
|
||||
|
||||
Rcon itself is insecure and each packet includes the password so I don't suggest using it remotely. If you have direct access to the server then SSH in first, then use this tool locally.
|
||||
|
||||
## Use
|
||||
---
|
||||
|
||||
`go get github.com/onyx-and-iris/q3rcon`
|
||||
## Package
|
||||
|
||||
#### Use
|
||||
|
||||
```console
|
||||
go get github.com/onyx-and-iris/q3rcon
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -91,12 +97,24 @@ rcon, err := q3rcon.New(
|
||||
q3rcon.WithTimeouts(timeouts))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Command line
|
||||
|
||||
Pass `host`, `port` and `rconpass` as flags, for example:
|
||||
### Install
|
||||
|
||||
```console
|
||||
go install github.com/onyx-and-iris/q3rcon/cmd/q3rcon@latest
|
||||
```
|
||||
q3rcon -H=localhost -p=30000 -r="rconpassword" "mapname"
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Flags
|
||||
|
||||
Pass `--host`, `--port` and `--rconpass` as flags, for example:
|
||||
|
||||
```console
|
||||
q3rcon --host=localhost --port=30000 --rconpass="rconpassword" "mapname"
|
||||
```
|
||||
|
||||
- `host` defaults to "localhost"
|
||||
@ -105,25 +123,41 @@ q3rcon -H=localhost -p=30000 -r="rconpassword" "mapname"
|
||||
|
||||
Arguments following the flags will be sent as rcon commands. You may send multiple arguments.
|
||||
|
||||
#### Interactive mode
|
||||
#### Environment Variables
|
||||
|
||||
example .envrc:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export Q3RCON_HOST="localhost"
|
||||
export Q3RCON_PORT=28960
|
||||
export Q3RCON_RCONPASS="rconpassword"
|
||||
```
|
||||
|
||||
### Interactive mode
|
||||
|
||||
Pass `interactive (-i shorthand)` flag to enable interactive mode, for example:
|
||||
|
||||
```bash
|
||||
q3rcon -h=localhost -p=30000 -r="rconpassword" -i
|
||||
```console
|
||||
q3rcon -H=localhost -p=30000 -r="rconpassword" -i
|
||||
```
|
||||
|
||||
If interactive mode is enabled, any arguments sent on the command line will be ignored.
|
||||
|
||||
---
|
||||
|
||||
## Your own implementation
|
||||
|
||||
The included CLI is a generic implementation, while it can be used out of the box you may find that some requests result in fragmented responses. The solution is to implement your own version, adjusting the timings with the functional options as detailed above. I could have increased the default timeouts but that would add unnecessary delay for most requests, so I decided to leave those details to the users of the package.
|
||||
The included CLI is a generic implementation, while it can be used out of the box you may find that some requests result in fragmented responses. The solution is to implement your own version, adjusting the timings with the functional options as detailed above.
|
||||
|
||||
Since you can include the q3rcon package into your own package you can easily make your own modifications, for example, I added [colour to the terminal][status] and [reformatted some of the responses][mapname].
|
||||
Since you can include the q3rcon package into your own CLI/package you can easily make your own modifications, for example, I added [colour to the terminal][status] and [tabulated some of the responses][mapname].
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
The `-loglevel` flag allows you to control the verbosity of the application's logging output.
|
||||
The `--loglevel` flag allows you to control the verbosity of the application's logging output.
|
||||
|
||||
Acceptable values for this flag are:
|
||||
|
||||
@ -137,8 +171,8 @@ Acceptable values for this flag are:
|
||||
|
||||
For example, to set the log level to `debug`, you can use:
|
||||
|
||||
```bash
|
||||
q3rcon -p=28960 -r="rconpassword" -loglevel=debug -i
|
||||
```console
|
||||
q3rcon -H=localhost -p=28960 -r="rconpassword" -l=debug -i
|
||||
```
|
||||
|
||||
The default log level is `warn` if the flag is not specified.
|
||||
|
||||
@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -11,12 +11,41 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/peterbourgon/ff/v4"
|
||||
"github.com/peterbourgon/ff/v4/ffhelp"
|
||||
|
||||
"github.com/onyx-and-iris/q3rcon"
|
||||
)
|
||||
|
||||
var version string // Version will be set at build time
|
||||
|
||||
type Flags struct {
|
||||
Host string
|
||||
Port int
|
||||
Rconpass string
|
||||
Interactive bool
|
||||
LogLevel string
|
||||
Version bool
|
||||
}
|
||||
|
||||
func (f Flags) Validate() error {
|
||||
if f.Port < 1024 || f.Port > 65535 {
|
||||
return fmt.Errorf(
|
||||
"invalid port value, got: (%d) expected: in range 1024-65535",
|
||||
f.Port,
|
||||
)
|
||||
}
|
||||
|
||||
if len(f.Rconpass) < 8 {
|
||||
return fmt.Errorf(
|
||||
"invalid rcon password, got: (%s) expected: at least 8 characters",
|
||||
f.Rconpass,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var exitCode int
|
||||
|
||||
@ -39,71 +68,67 @@ func main() {
|
||||
|
||||
// run executes the main logic of the application and returns a cleanup function and an error if any.
|
||||
func run() (func(), error) {
|
||||
var (
|
||||
host string
|
||||
port int
|
||||
rconpass string
|
||||
interactive bool
|
||||
loglevel string
|
||||
versionFlag bool
|
||||
)
|
||||
var flags Flags
|
||||
|
||||
flag.StringVar(&host, "host", "localhost", "hostname of the gameserver")
|
||||
flag.StringVar(&host, "H", "localhost", "hostname of the gameserver (shorthand)")
|
||||
flag.IntVar(&port, "port", 28960, "port on which the gameserver resides, default is 28960")
|
||||
flag.IntVar(
|
||||
&port,
|
||||
"p",
|
||||
fs := ff.NewFlagSet("q3rcon - A command-line RCON client for Q3 Rcon compatible game servers")
|
||||
fs.StringVar(&flags.Host, 'H', "host", "localhost", "hostname of the gameserver")
|
||||
fs.IntVar(
|
||||
&flags.Port,
|
||||
'p',
|
||||
"port",
|
||||
28960,
|
||||
"port on which the gameserver resides, default is 28960 (shorthand)",
|
||||
"port on which the gameserver resides, default is 28960",
|
||||
)
|
||||
flag.StringVar(&rconpass, "rconpass", os.Getenv("RCON_PASS"), "rcon password of the gameserver")
|
||||
flag.StringVar(
|
||||
&rconpass,
|
||||
"r",
|
||||
os.Getenv("RCON_PASS"),
|
||||
"rcon password of the gameserver (shorthand)",
|
||||
fs.StringVar(
|
||||
&flags.Rconpass,
|
||||
'r',
|
||||
"rconpass",
|
||||
"",
|
||||
"rcon password of the gameserver",
|
||||
)
|
||||
|
||||
flag.BoolVar(&interactive, "interactive", false, "run in interactive mode")
|
||||
flag.BoolVar(&interactive, "i", false, "run in interactive mode (shorthand)")
|
||||
fs.BoolVar(&flags.Interactive, 'i', "interactive", "run in interactive mode")
|
||||
fs.StringVar(
|
||||
&flags.LogLevel,
|
||||
'l',
|
||||
"loglevel",
|
||||
"info",
|
||||
"Log level (debug, info, warn, error, fatal, panic)",
|
||||
)
|
||||
fs.BoolVar(&flags.Version, 'v', "version", "print version information and exit")
|
||||
|
||||
flag.StringVar(&loglevel, "loglevel", "warn", "log level")
|
||||
flag.StringVar(&loglevel, "l", "warn", "log level (shorthand)")
|
||||
err := ff.Parse(fs, os.Args[1:],
|
||||
ff.WithEnvVarPrefix("Q3RCON"),
|
||||
)
|
||||
switch {
|
||||
case errors.Is(err, ff.ErrHelp):
|
||||
fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Flags(fs, "q3rcon [flags] <rcon commands>"))
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("failed to parse flags: %w", err)
|
||||
}
|
||||
|
||||
flag.BoolVar(&versionFlag, "version", false, "print version information and exit")
|
||||
flag.BoolVar(&versionFlag, "v", false, "print version information and exit (shorthand)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
if flags.Version {
|
||||
fmt.Printf("q3rcon version: %s\n", versionFromBuild())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
level, err := log.ParseLevel(loglevel)
|
||||
if err := flags.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
level, err := log.ParseLevel(flags.LogLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid log level: %s", loglevel)
|
||||
return nil, fmt.Errorf("invalid log level: %s", flags.LogLevel)
|
||||
}
|
||||
log.SetLevel(level)
|
||||
|
||||
if port < 1024 || port > 65535 {
|
||||
return nil, fmt.Errorf("invalid port value, got: (%d) expected: in range 1024-65535", port)
|
||||
}
|
||||
|
||||
if len(rconpass) < 8 {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid rcon password, got: (%s) expected: at least 8 characters",
|
||||
rconpass,
|
||||
)
|
||||
}
|
||||
|
||||
client, closer, err := connectRcon(host, port, rconpass)
|
||||
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
|
||||
}
|
||||
|
||||
if interactive {
|
||||
if flags.Interactive {
|
||||
fmt.Printf("Enter 'Q' to exit.\n>> ")
|
||||
err := interactiveMode(client, os.Stdin)
|
||||
if err != nil {
|
||||
@ -112,7 +137,7 @@ func run() (func(), error) {
|
||||
return closer, nil
|
||||
}
|
||||
|
||||
commands := flag.Args()
|
||||
commands := fs.GetArgs()
|
||||
if len(commands) == 0 {
|
||||
log.Debug("no commands provided, defaulting to 'status'")
|
||||
commands = append(commands, "status")
|
||||
|
||||
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
|
||||
6
go.sum
6
go.sum
@ -25,6 +25,10 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/peterbourgon/ff/v4 v4.0.0-beta.1 h1:hV8qRu3V7YfiSMsBSfPfdcznAvPQd3jI5zDddSrDoUc=
|
||||
github.com/peterbourgon/ff/v4 v4.0.0-beta.1/go.mod h1:onQJUKipvCyFmZ1rIYwFAh1BhPOvftb1uhvSI7krNLc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@ -45,6 +49,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user