mirror of
https://github.com/onyx-and-iris/q3rcon.git
synced 2026-03-02 17:09:19 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f552b96737 | |||
| 6d321aa816 | |||
| eeb2c74387 | |||
| c4e12fcaf7 | |||
| cf82b29de5 | |||
| b1161e1e97 | |||
| f74fbedacc | |||
| 3f45588afb | |||
| caffd65cb3 |
@ -11,7 +11,13 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
# [0.5.0] - 2026-02-17
|
# [0.5.3] - 2026-02-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Spinner for long running CLI commands.
|
||||||
|
|
||||||
|
# [0.5.1] - 2026-02-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@ -21,6 +27,7 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- The CLI now supports `--long` and `-short` style flags. Several examples in README.
|
- The CLI now supports `--long` and `-short` style flags. Several examples in README.
|
||||||
- `--help` output has been improved.
|
- `--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
|
# [0.4.1] - 2026-02-15
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/chelnak/ysmrr"
|
||||||
"github.com/peterbourgon/ff/v4"
|
"github.com/peterbourgon/ff/v4"
|
||||||
"github.com/peterbourgon/ff/v4/ffhelp"
|
"github.com/peterbourgon/ff/v4/ffhelp"
|
||||||
|
|
||||||
@ -47,6 +48,8 @@ func (f Flags) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reColourCodes = regexp.MustCompile(`\^[0-9]`)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var exitCode int
|
var exitCode int
|
||||||
|
|
||||||
@ -67,6 +70,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type context struct {
|
||||||
|
client *q3rcon.Rcon
|
||||||
|
timeouts map[string]time.Duration
|
||||||
|
in io.Reader
|
||||||
|
sm ysmrr.SpinnerManager
|
||||||
|
}
|
||||||
|
|
||||||
// run executes the main logic of the application and returns a cleanup function and an error if any.
|
// run executes the main logic of the application and returns a cleanup function and an error if any.
|
||||||
func run() (func(), error) {
|
func run() (func(), error) {
|
||||||
var flags Flags
|
var flags Flags
|
||||||
@ -124,14 +134,31 @@ func run() (func(), error) {
|
|||||||
}
|
}
|
||||||
log.SetLevel(level)
|
log.SetLevel(level)
|
||||||
|
|
||||||
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass)
|
timeouts := map[string]time.Duration{
|
||||||
|
"map": time.Second,
|
||||||
|
"map_rotate": time.Second,
|
||||||
|
"map_restart": time.Second,
|
||||||
|
}
|
||||||
|
log.Debugf("using timeouts: %v", timeouts)
|
||||||
|
|
||||||
|
client, closer, err := connectRcon(flags.Host, flags.Port, flags.Rconpass, timeouts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
|
return nil, fmt.Errorf("failed to connect to rcon: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sm := ysmrr.NewSpinnerManager()
|
||||||
|
sm.AddSpinner("")
|
||||||
|
|
||||||
|
ctx := &context{
|
||||||
|
client: client,
|
||||||
|
timeouts: timeouts,
|
||||||
|
in: os.Stdin,
|
||||||
|
sm: sm,
|
||||||
|
}
|
||||||
|
|
||||||
if flags.Interactive {
|
if flags.Interactive {
|
||||||
fmt.Printf("Enter 'Q' to exit.\n>> ")
|
fmt.Printf("Enter 'Q' to exit.\n>> ")
|
||||||
err := interactiveMode(client, os.Stdin)
|
err := interactiveMode(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return closer, fmt.Errorf("interactive mode error: %w", err)
|
return closer, fmt.Errorf("interactive mode error: %w", err)
|
||||||
}
|
}
|
||||||
@ -143,7 +170,7 @@ func run() (func(), error) {
|
|||||||
log.Debug("no commands provided, defaulting to 'status'")
|
log.Debug("no commands provided, defaulting to 'status'")
|
||||||
commands = append(commands, "status")
|
commands = append(commands, "status")
|
||||||
}
|
}
|
||||||
runCommands(client, commands)
|
runCommands(ctx, commands)
|
||||||
|
|
||||||
return closer, nil
|
return closer, nil
|
||||||
}
|
}
|
||||||
@ -161,12 +188,13 @@ func versionFromBuild() string {
|
|||||||
return strings.Split(info.Main.Version, "-")[0]
|
return strings.Split(info.Main.Version, "-")[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, func(), error) {
|
func connectRcon(
|
||||||
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(map[string]time.Duration{
|
host string,
|
||||||
"map": time.Second,
|
port int,
|
||||||
"map_rotate": time.Second,
|
password string,
|
||||||
"map_restart": time.Second,
|
timeouts map[string]time.Duration,
|
||||||
}))
|
) (*q3rcon.Rcon, func(), error) {
|
||||||
|
client, err := q3rcon.New(host, port, password, q3rcon.WithTimeouts(timeouts))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@ -180,34 +208,19 @@ func connectRcon(host string, port int, password string) (*q3rcon.Rcon, func(),
|
|||||||
return client, closer, nil
|
return client, closer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runCommands runs the commands given in the flag.Args slice.
|
|
||||||
// If no commands are given, it defaults to running the "status" command.
|
|
||||||
func runCommands(client *q3rcon.Rcon, commands []string) {
|
|
||||||
for _, cmd := range commands {
|
|
||||||
resp, err := client.Send(cmd)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Print(removeColourCodes(resp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// interactiveMode continuously reads from input until a quit signal is given.
|
// interactiveMode continuously reads from input until a quit signal is given.
|
||||||
func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
|
func interactiveMode(ctx *context) error {
|
||||||
scanner := bufio.NewScanner(input)
|
scanner := bufio.NewScanner(ctx.in)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
cmd := scanner.Text()
|
cmd := scanner.Text()
|
||||||
if strings.EqualFold(cmd, "Q") {
|
if strings.EqualFold(cmd, "Q") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Send(cmd)
|
if err := runCommand(ctx, cmd); err != nil {
|
||||||
if err != nil {
|
fmt.Printf("Error: %v\n", err)
|
||||||
log.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
fmt.Printf("%s>> ", removeColourCodes(resp))
|
fmt.Printf(">> ")
|
||||||
}
|
}
|
||||||
|
|
||||||
if scanner.Err() != nil {
|
if scanner.Err() != nil {
|
||||||
@ -216,9 +229,31 @@ func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var colourCodeRegex = regexp.MustCompile(`\^[0-9]`)
|
// runCommands executes a list of commands sequentially and prints any errors encountered.
|
||||||
|
func runCommands(ctx *context, commands []string) {
|
||||||
// removeColourCodes removes '\^[0-9]' colour codes from the input string.
|
for _, cmd := range commands {
|
||||||
func removeColourCodes(s string) string {
|
if err := runCommand(ctx, cmd); err != nil {
|
||||||
return colourCodeRegex.ReplaceAllString(s, "")
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand sends a command to the RCON client and prints the response.
|
||||||
|
// If the command is in the timeouts map, it starts a spinner until the response is received.
|
||||||
|
func runCommand(ctx *context, cmd string) error {
|
||||||
|
before, _, _ := strings.Cut(cmd, " ")
|
||||||
|
_, ok := ctx.timeouts[before]
|
||||||
|
if ok {
|
||||||
|
ctx.sm.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ctx.client.Send(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run command '%s': %w", cmd, err)
|
||||||
|
}
|
||||||
|
if ctx.sm.Running() {
|
||||||
|
ctx.sm.Stop()
|
||||||
|
}
|
||||||
|
fmt.Print(reColourCodes.ReplaceAllString(resp, ""))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
package conn
|
package q3rcon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
)
|
)
|
||||||
@ -12,23 +11,23 @@ type UDPConn struct {
|
|||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(host string, port int) (UDPConn, error) {
|
func newUDPConn(host string, port int) (*UDPConn, error) {
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
|
udpAddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UDPConn{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UDPConn{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Infof("Outgoing address %s", conn.RemoteAddr())
|
log.Infof("Outgoing address %s", conn.RemoteAddr())
|
||||||
|
|
||||||
return UDPConn{
|
return &UDPConn{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Write(buf []byte) (int, error) {
|
func (c *UDPConn) Write(buf []byte) (int, error) {
|
||||||
n, err := c.conn.Write(buf)
|
n, err := c.conn.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -37,8 +36,7 @@ func (c UDPConn) Write(buf []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
|
func (c *UDPConn) Read(buf []byte) (int, error) {
|
||||||
c.conn.SetReadDeadline(timeout)
|
|
||||||
rlen, _, err := c.conn.ReadFromUDP(buf)
|
rlen, _, err := c.conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -46,7 +44,7 @@ func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
|
|||||||
return rlen, nil
|
return rlen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Close() error {
|
func (c *UDPConn) Close() error {
|
||||||
err := c.conn.Close()
|
err := c.conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
6
go.mod
6
go.mod
@ -4,6 +4,7 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
|
github.com/chelnak/ysmrr v0.6.0
|
||||||
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
|
github.com/peterbourgon/ff/v4 v4.0.0-beta.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,13 +15,16 @@ require (
|
|||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/term v0.40.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -12,12 +12,18 @@ 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/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 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/chelnak/ysmrr v0.6.0 h1:kMhO0oI02tl/9szvxrOE0yeImtrK4KQhER0oXu1K/iM=
|
||||||
|
github.com/chelnak/ysmrr v0.6.0/go.mod h1:56JSrmQgb7/7xoMvuD87h3PE/qW6K1+BQcrgWtVLTUo=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
@ -40,8 +46,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
|
|||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
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/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/sys v0.6.0/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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
package packet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
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, "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.Debugf("Encoded request: %s", r.buf.String())
|
|
||||||
return r.buf.Bytes()
|
|
||||||
}
|
|
||||||
@ -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, "print\n"...)
|
|
||||||
}
|
|
||||||
60
q3rcon.go
60
q3rcon.go
@ -1,24 +1,31 @@
|
|||||||
package q3rcon
|
package q3rcon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/conn"
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/packet"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const respBufSiz = 2048
|
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 {
|
type Rcon struct {
|
||||||
conn conn.UDPConn
|
conn io.ReadWriteCloser
|
||||||
request packet.Request
|
request encoder
|
||||||
response packet.Response
|
response decoder
|
||||||
|
|
||||||
loginTimeout time.Duration
|
loginTimeout time.Duration
|
||||||
defaultTimeout 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")
|
return nil, errors.New("no password provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := conn.New(host, port)
|
conn, err := newUDPConn(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error creating UDP connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &Rcon{
|
r := &Rcon{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
request: packet.NewRequest(password),
|
request: newRequest(password),
|
||||||
response: packet.NewResponse(),
|
response: newResponse(),
|
||||||
|
|
||||||
loginTimeout: 5 * time.Second,
|
loginTimeout: 5 * time.Second,
|
||||||
defaultTimeout: 20 * time.Millisecond,
|
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 {
|
if err = r.login(); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error logging in: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
@ -65,7 +72,7 @@ func (r Rcon) login() error {
|
|||||||
default:
|
default:
|
||||||
resp, err := r.Send("login")
|
resp, err := r.Send("login")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("error sending login command: %w", err)
|
||||||
}
|
}
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
continue
|
continue
|
||||||
@ -94,9 +101,14 @@ func (r Rcon) Send(cmdWithArgs string) (string, error) {
|
|||||||
|
|
||||||
go r.listen(timeout, respChan, errChan)
|
go r.listen(timeout, respChan, errChan)
|
||||||
|
|
||||||
_, err := r.conn.Write(r.request.Encode(cmdWithArgs))
|
encodedCmd, err := r.request.encode(cmdWithArgs)
|
||||||
if err != nil {
|
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 {
|
select {
|
||||||
@ -118,7 +130,17 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
|
|||||||
respChan <- sb.String()
|
respChan <- sb.String()
|
||||||
return
|
return
|
||||||
default:
|
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 {
|
if err != nil {
|
||||||
e, ok := err.(net.Error)
|
e, ok := err.(net.Error)
|
||||||
if ok {
|
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 r.response.isValid(respBuf[:rlen]) {
|
||||||
if bytes.HasPrefix(respBuf, r.response.Header()) {
|
sb.WriteString(r.response.decode(respBuf[:rlen]))
|
||||||
sb.Write(respBuf[len(r.response.Header()):rlen])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
request.go
Normal file
35
request.go
Normal 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
21
response.go
Normal 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):])
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user