16 Commits

Author SHA1 Message Date
b1161e1e97 unexport request, response methods 2026-02-19 00:25:47 +00:00
f74fbedacc print input prompt on error (for next command). 2026-02-19 00:23:03 +00:00
3f45588afb remove internal packages, move everything into q3rcon
{request}.buf initialised with 0 length.

encoder, decoder interfaces added.
2026-02-19 00:22:32 +00:00
caffd65cb3 upd CHANGELOG 2026-02-18 10:45:27 +00:00
6b33882c0c remove colour codes from output strings 2026-02-18 10:42:04 +00:00
abb33742aa replace logrus with charm/log 2026-02-18 10:41:42 +00:00
51e8ac85be use fs.GetArgs() 2026-02-17 16:41:42 +00:00
79d53f34da reword 2026-02-17 16:37:25 +00:00
7c5a3523bf fix install command 2026-02-17 16:34:09 +00:00
44a528e31d add v0.5.0 to CHANGELOG 2026-02-17 16:30:14 +00:00
48b23321e5 fix help usage 2026-02-17 16:26:31 +00:00
d05ed91473 fix NAME desc in help output 2026-02-17 16:13:50 +00:00
5c28c4e8b7 upd date 2026-02-17 16:10:57 +00:00
53f30981fd bump version in CHANGELOG
add note about CLI env vars
2026-02-17 16:10:09 +00:00
fce6fa43fc Reimplement the CLI with flags first package.
CLI configuration may now be managed with env vars.

improved CLI help output
2026-02-17 16:09:53 +00:00
0b9546ee0e update README 2026-02-17 16:06:58 +00:00
12 changed files with 259 additions and 154 deletions

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ go.work.sum
# env file
.env
.envrc
# Added by goreleaser init:
dist/

View File

@@ -11,7 +11,20 @@ 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.1] - 2026-02-18
### 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.
- Colour codes have been removed from CLI output. This makes the responses easier to read.
# [0.4.1] - 2026-02-15
### Added

View File

@@ -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.

View File

@@ -2,21 +2,51 @@ package main
import (
"bufio"
"flag"
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime/debug"
"strings"
"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 +69,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 +138,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")
@@ -163,7 +189,7 @@ func runCommands(client *q3rcon.Rcon, commands []string) {
log.Error(err)
continue
}
fmt.Print(resp)
fmt.Print(removeColourCodes(resp))
}
}
@@ -179,9 +205,10 @@ func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
resp, err := client.Send(cmd)
if err != nil {
log.Error(err)
fmt.Print(">> ")
continue
}
fmt.Printf("%s>> ", resp)
fmt.Printf("%s>> ", removeColourCodes(resp))
}
if scanner.Err() != nil {
@@ -189,3 +216,10 @@ func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
}
return nil
}
var colourCodeRegex = regexp.MustCompile(`\^[0-9]`)
// removeColourCodes removes '\^[0-9]' colour codes from the input string.
func removeColourCodes(s string) string {
return colourCodeRegex.ReplaceAllString(s, "")
}

View File

@@ -1,34 +1,33 @@
package conn
package q3rcon
import (
"fmt"
"net"
"time"
log "github.com/sirupsen/logrus"
"github.com/charmbracelet/log"
)
type UDPConn struct {
conn *net.UDPConn
}
func New(host string, port int) (UDPConn, error) {
udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
func newUDPConn(host string, port int) (*UDPConn, error) {
udpAddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
if err != nil {
return UDPConn{}, err
return nil, err
}
conn, err := net.DialUDP("udp4", nil, udpAddr)
if err != nil {
return UDPConn{}, err
return nil, err
}
log.Infof("Outgoing address %s", conn.RemoteAddr())
return UDPConn{
return &UDPConn{
conn: conn,
}, nil
}
func (c UDPConn) Write(buf []byte) (int, error) {
func (c *UDPConn) Write(buf []byte) (int, error) {
n, err := c.conn.Write(buf)
if err != nil {
return 0, err
@@ -37,8 +36,7 @@ func (c UDPConn) Write(buf []byte) (int, error) {
return n, nil
}
func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
c.conn.SetReadDeadline(timeout)
func (c *UDPConn) Read(buf []byte) (int, error) {
rlen, _, err := c.conn.ReadFromUDP(buf)
if err != nil {
return 0, err
@@ -46,7 +44,7 @@ func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
return rlen, nil
}
func (c UDPConn) Close() error {
func (c *UDPConn) Close() error {
err := c.conn.Close()
if err != nil {
return err

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.0
require (
github.com/charmbracelet/log v0.4.2
github.com/sirupsen/logrus v1.9.3
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
)
require (

14
go.sum
View File

@@ -12,7 +12,6 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -25,26 +24,25 @@ 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=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,36 +0,0 @@
package packet
import (
"bytes"
"fmt"
log "github.com/sirupsen/logrus"
)
const bufSz = 512
type Request struct {
magic []byte
password string
buf *bytes.Buffer
}
func NewRequest(password string) Request {
return Request{
magic: []byte{'\xff', '\xff', '\xff', '\xff'},
password: password,
buf: bytes.NewBuffer(make([]byte, bufSz)),
}
}
func (r Request) Header() []byte {
return append(r.magic, []byte("rcon")...)
}
func (r Request) Encode(cmd string) []byte {
r.buf.Reset()
r.buf.Write(r.Header())
r.buf.WriteString(fmt.Sprintf(" %s %s", r.password, cmd))
log.Tracef("Encoded request: %s", r.buf.String())
return r.buf.Bytes()
}

View File

@@ -1,13 +0,0 @@
package packet
type Response struct {
magic []byte
}
func NewResponse() Response {
return Response{magic: []byte{'\xff', '\xff', '\xff', '\xff'}}
}
func (r Response) Header() []byte {
return append(r.magic, []byte("print\n")...)
}

View File

@@ -1,24 +1,31 @@
package q3rcon
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/onyx-and-iris/q3rcon/internal/conn"
"github.com/onyx-and-iris/q3rcon/internal/packet"
"github.com/charmbracelet/log"
)
const respBufSiz = 2048
type encoder interface {
encode(cmd string) ([]byte, error)
}
type decoder interface {
isValid(buf []byte) bool
decode(buf []byte) string
}
type Rcon struct {
conn conn.UDPConn
request packet.Request
response packet.Response
conn io.ReadWriteCloser
request encoder
response decoder
loginTimeout time.Duration
defaultTimeout time.Duration
@@ -30,15 +37,15 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
return nil, errors.New("no password provided")
}
conn, err := conn.New(host, port)
conn, err := newUDPConn(host, port)
if err != nil {
return nil, err
return nil, fmt.Errorf("error creating UDP connection: %w", err)
}
r := &Rcon{
conn: conn,
request: packet.NewRequest(password),
response: packet.NewResponse(),
request: newRequest(password),
response: newResponse(),
loginTimeout: 5 * time.Second,
defaultTimeout: 20 * time.Millisecond,
@@ -50,7 +57,7 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
}
if err = r.login(); err != nil {
return nil, err
return nil, fmt.Errorf("error logging in: %w", err)
}
return r, nil
@@ -65,7 +72,7 @@ func (r Rcon) login() error {
default:
resp, err := r.Send("login")
if err != nil {
return err
return fmt.Errorf("error sending login command: %w", err)
}
if resp == "" {
continue
@@ -94,9 +101,14 @@ func (r Rcon) Send(cmdWithArgs string) (string, error) {
go r.listen(timeout, respChan, errChan)
_, err := r.conn.Write(r.request.Encode(cmdWithArgs))
encodedCmd, err := r.request.encode(cmdWithArgs)
if err != nil {
return "", err
return "", fmt.Errorf("error encoding command: %w", err)
}
_, err = r.conn.Write(encodedCmd)
if err != nil {
return "", fmt.Errorf("error writing command to connection: %w", err)
}
select {
@@ -118,7 +130,17 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
respChan <- sb.String()
return
default:
rlen, err := r.conn.ReadUntil(time.Now().Add(timeout), respBuf)
c, ok := r.conn.(*UDPConn)
if !ok {
errChan <- errors.New("connection is not a UDPConn")
return
}
err := c.conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
errChan <- fmt.Errorf("error setting read deadline: %w", err)
return
}
rlen, err := r.conn.Read(respBuf)
if err != nil {
e, ok := err.(net.Error)
if ok {
@@ -131,10 +153,8 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
}
}
if rlen > len(r.response.Header()) {
if bytes.HasPrefix(respBuf, r.response.Header()) {
sb.Write(respBuf[len(r.response.Header()):rlen])
}
if r.response.isValid(respBuf[:rlen]) {
sb.WriteString(r.response.decode(respBuf[:rlen]))
}
}
}

35
request.go Normal file
View File

@@ -0,0 +1,35 @@
package q3rcon
import (
"bytes"
"errors"
"fmt"
)
const (
bufSz = 1024
requestHeader = "\xff\xff\xff\xffrcon"
)
type request struct {
password string
buf *bytes.Buffer
}
func newRequest(password string) request {
return request{
password: password,
buf: bytes.NewBuffer(make([]byte, 0, bufSz)),
}
}
func (r request) encode(cmd string) ([]byte, error) {
if cmd == "" {
return nil, errors.New("command cannot be empty")
}
r.buf.Reset()
r.buf.WriteString(requestHeader)
r.buf.WriteString(fmt.Sprintf(" %s %s", r.password, cmd))
return r.buf.Bytes(), nil
}

21
response.go Normal file
View File

@@ -0,0 +1,21 @@
package q3rcon
import "bytes"
const (
responseHeader = "\xff\xff\xff\xffprint\n"
)
type response struct{}
func newResponse() response {
return response{}
}
func (r response) isValid(buf []byte) bool {
return len(buf) > len(responseHeader) && bytes.HasPrefix(buf, []byte(responseHeader))
}
func (r response) decode(buf []byte) string {
return string(buf[len(responseHeader):])
}