mirror of
https://github.com/onyx-and-iris/q3rcon.git
synced 2026-04-20 00:33:31 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bc19a718b | |||
| adebc61b98 | |||
| 51ff562ac4 | |||
| 313d96fffa | |||
| cbc8ee5543 | |||
| 3e1088d625 | |||
| 65ab17b9c9 | |||
| d180c455a3 | |||
| b20bca0c77 | |||
| 7d787324a7 | |||
| 8c1bc0f04c | |||
| dd1d530d44 | |||
| f2752f5609 | |||
| 05ee3f1912 | |||
| a7702c8050 | |||
| 08af9d3388 | |||
| 46d3e3fa4a | |||
| 07ffd98e7a | |||
| bc373e72ae |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,4 +25,4 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
cmd/aeiou
|
cmd/codrcon
|
||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -11,9 +11,26 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
|
# [0.1.0] - 2024-11-29
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `-P` flag changed to `-r` for setting rcon password. This is to disambiguate it from the port (-p) flag.
|
||||||
|
|
||||||
|
# [0.0.3] - 2024-11-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- {Rcon}.login is no longer exported since it's called internally by the constructor.
|
||||||
|
- When checking the timeouts map the cmd is split from its arguments. This allows setting a timeout value for all `map mp_` for example.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Timeout values for commands in the timeouts map are now logged at Debug level.
|
||||||
|
|
||||||
# [0.0.1] - 2024-11-04
|
# [0.0.1] - 2024-11-04
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial release, package implements Rcon using the Q3 procotocl.
|
- Initial release, package implements Rcon using the Q3 protocol.
|
||||||
- A basic CLI implementation accepting configuration flags.
|
- A basic CLI implementation accepting configuration flags.
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Onyx and Iris
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
43
README.md
43
README.md
@@ -52,14 +52,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `WithDefaultTimeout(timeout time.Duration)`
|
#### `WithLoginTimeout(timeout time.Duration)`
|
||||||
|
|
||||||
You may want to change the default timeout if some of your responses are getting mixed together (stability can depend on connection to the server). For example, on LAN I can leave the default at 20ms, when connecting remotely I normally increase this to 50ms.
|
If the server was just started or is currently performing a long operation like map rotating then it's possible to receive empty rcon responses. In which case you'll want to retry login. Use this functional option to set the max timeout for logins, it defaults to 5 seconds.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
rcon, err := q3rcon.New(
|
rcon, err := q3rcon.New(
|
||||||
|
host, port, password,
|
||||||
|
q3rcon.WithLoginTimeout(2*time.Second))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `WithDefaultTimeout(timeout time.Duration)`
|
||||||
|
|
||||||
|
You may want to change the default timeout if some of your responses are getting mixed together (stability can depend on connection to the server). For example, over localhost I can leave the default at 20ms, when connecting remotely I normally increase this to 50ms.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rcon, err := q3rcon.New(
|
||||||
host, port, password,
|
host, port, password,
|
||||||
q3rcon.WithDefaultTimeout(50*time.Millisecond))
|
q3rcon.WithDefaultTimeout(50*time.Millisecond))
|
||||||
```
|
```
|
||||||
@@ -69,27 +81,27 @@ For example:
|
|||||||
Perhaps there are some requests that take a long time to receive a response but you want the whole response in one chunk. Pass a timeouts map, for example:
|
Perhaps there are some requests that take a long time to receive a response but you want the whole response in one chunk. Pass a timeouts map, for example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
timeouts := map[string]time.Duration{
|
timeouts := map[string]time.Duration{
|
||||||
"map_rotate": 1200 * time.Millisecond,
|
"map_rotate": 1200 * time.Millisecond,
|
||||||
"map_restart": 1200 * time.Millisecond,
|
"map_restart": 1200 * time.Millisecond,
|
||||||
}
|
}
|
||||||
|
|
||||||
rcon, err := q3rcon.New(
|
rcon, err := q3rcon.New(
|
||||||
host, port, password,
|
host, port, password,
|
||||||
q3rcon.WithTimeouts(timeouts))
|
q3rcon.WithTimeouts(timeouts))
|
||||||
```
|
```
|
||||||
|
|
||||||
## Command line
|
## Command line
|
||||||
|
|
||||||
Pass `host`, `port` and `password` as flags, for example:
|
Pass `host`, `port` and `rconpass` as flags, for example:
|
||||||
|
|
||||||
```
|
```
|
||||||
q3rcon -h=localhost -p=30000 -P="rconpassword" "mapname"
|
q3rcon -h=localhost -p=30000 -r="rconpassword" "mapname"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `host` defaults to "localhost"
|
- `host` defaults to "localhost"
|
||||||
- `port` defaults to 28960
|
- `port` defaults to 28960
|
||||||
- `password` defaults to ""
|
- `rconpass` defaults to ""
|
||||||
|
|
||||||
Arguments following the flags will be sent as rcon commands. You may send multiple arguments.
|
Arguments following the flags will be sent as rcon commands. You may send multiple arguments.
|
||||||
|
|
||||||
@@ -98,13 +110,22 @@ Arguments following the flags will be sent as rcon commands. You may send multip
|
|||||||
Pass `interactive (-i shorthand)` flag to enable interactive mode, for example:
|
Pass `interactive (-i shorthand)` flag to enable interactive mode, for example:
|
||||||
|
|
||||||
```
|
```
|
||||||
q3rcon -h=localhost -p=30000 -P="rconpassword" -i
|
q3rcon -h=localhost -p=30000 -r="rconpassword" -i
|
||||||
```
|
```
|
||||||
|
|
||||||
If interactive mode is enabled, any arguments sent on the command line will be ignored.
|
If interactive mode is enabled, any arguments sent on the command line will be ignored.
|
||||||
|
|
||||||
## `Logging`
|
## 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.
|
||||||
|
|
||||||
|
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].
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
Log level may be set by passing the `-l` flag with a number from 0 up to 6 where
|
Log level may be set by passing the `-l` flag with a number from 0 up to 6 where
|
||||||
|
|
||||||
0 = Panic, 1 = Fatal, 2 = Error, 3 = Warning, 4 = Info, 5 = Debug, 6 = Trace
|
0 = Panic, 1 = Fatal, 2 = Error, 3 = Warning, 4 = Info, 5 = Debug, 6 = Trace
|
||||||
|
|
||||||
|
[status]: ./img/status.png
|
||||||
|
[mapname]: ./img/mapname.png
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,11 +17,17 @@ import (
|
|||||||
|
|
||||||
var interactive bool
|
var interactive bool
|
||||||
|
|
||||||
|
func exit(err error) {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
password string
|
rconpass string
|
||||||
loglevel int
|
loglevel int
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +35,8 @@ func main() {
|
|||||||
flag.StringVar(&host, "h", "localhost", "hostname of the server (shorthand)")
|
flag.StringVar(&host, "h", "localhost", "hostname of the server (shorthand)")
|
||||||
flag.IntVar(&port, "port", 28960, "port of the server")
|
flag.IntVar(&port, "port", 28960, "port of the server")
|
||||||
flag.IntVar(&port, "p", 28960, "port of the server (shorthand)")
|
flag.IntVar(&port, "p", 28960, "port of the server (shorthand)")
|
||||||
flag.StringVar(&password, "password", "", "hostname of the server")
|
flag.StringVar(&rconpass, "rconpass", "", "rcon password")
|
||||||
flag.StringVar(&password, "P", "", "hostname of the server (shorthand)")
|
flag.StringVar(&rconpass, "r", "", "rcon password (shorthand)")
|
||||||
|
|
||||||
flag.BoolVar(&interactive, "interactive", false, "run in interactive mode")
|
flag.BoolVar(&interactive, "interactive", false, "run in interactive mode")
|
||||||
flag.BoolVar(&interactive, "i", false, "run in interactive mode")
|
flag.BoolVar(&interactive, "i", false, "run in interactive mode")
|
||||||
@@ -42,7 +49,7 @@ func main() {
|
|||||||
log.SetLevel(log.Level(loglevel))
|
log.SetLevel(log.Level(loglevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
rcon, err := connectRcon(host, port, password)
|
rcon, err := connectRcon(host, port, rconpass)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -57,6 +64,11 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(flag.Args()) == 0 {
|
||||||
|
err = errors.New("no rcon commands passed")
|
||||||
|
exit(err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, arg := range flag.Args() {
|
for _, arg := range flag.Args() {
|
||||||
resp, err := rcon.Send(arg)
|
resp, err := rcon.Send(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,8 +80,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, error) {
|
func connectRcon(host string, port int, password string) (*q3rcon.Rcon, error) {
|
||||||
rcon, err := q3rcon.New(
|
rcon, err := q3rcon.New(host, port, password)
|
||||||
host, port, password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,7 +92,7 @@ func interactiveMode(rcon *q3rcon.Rcon, input io.Reader) error {
|
|||||||
scanner := bufio.NewScanner(input)
|
scanner := bufio.NewScanner(input)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
cmd := scanner.Text()
|
cmd := scanner.Text()
|
||||||
if strings.ToUpper(cmd) == "Q" {
|
if strings.EqualFold(cmd, "Q") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
img/mapname.png
Normal file
BIN
img/mapname.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.4 KiB |
BIN
img/status.png
Normal file
BIN
img/status.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -1,19 +1,15 @@
|
|||||||
package conn
|
package conn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/packet"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UDPConn struct {
|
type UDPConn struct {
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
response packet.Response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(host string, port int) (UDPConn, error) {
|
func New(host string, port int) (UDPConn, error) {
|
||||||
@@ -29,7 +25,6 @@ func New(host string, port int) (UDPConn, error) {
|
|||||||
|
|
||||||
return UDPConn{
|
return UDPConn{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
response: packet.NewResponse(),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,39 +37,13 @@ func (c UDPConn) Write(buf []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Listen(timeout time.Duration, resp chan<- string, errChan chan<- error) {
|
func (c UDPConn) ReadUntil(timeout time.Time, buf []byte) (int, error) {
|
||||||
c.conn.SetReadDeadline(time.Now().Add(timeout))
|
c.conn.SetReadDeadline(timeout)
|
||||||
done := make(chan struct{})
|
|
||||||
var sb strings.Builder
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
resp <- sb.String()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
rlen, _, err := c.conn.ReadFromUDP(buf)
|
rlen, _, err := c.conn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e, ok := err.(net.Error)
|
return 0, err
|
||||||
if ok {
|
|
||||||
if e.Timeout() {
|
|
||||||
close(done)
|
|
||||||
} else {
|
|
||||||
errChan <- e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rlen < len(c.response.Header()) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.HasPrefix(buf, c.response.Header()) {
|
|
||||||
sb.Write(buf[len(c.response.Header()):rlen])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return rlen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c UDPConn) Close() error {
|
func (c UDPConn) Close() error {
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package packet
|
package packet
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bufSz = 512
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
magic []byte
|
magic []byte
|
||||||
password string
|
password string
|
||||||
|
buf *bytes.Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRequest(password string) Request {
|
func NewRequest(password string) Request {
|
||||||
return Request{
|
return Request{
|
||||||
magic: []byte{'\xff', '\xff', '\xff', '\xff'},
|
magic: []byte{'\xff', '\xff', '\xff', '\xff'},
|
||||||
password: password,
|
password: password,
|
||||||
|
buf: bytes.NewBuffer(make([]byte, bufSz)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +26,8 @@ func (r Request) Header() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r Request) Encode(cmd string) []byte {
|
func (r Request) Encode(cmd string) []byte {
|
||||||
datagram := r.Header()
|
r.buf.Reset()
|
||||||
datagram = append(datagram, fmt.Sprintf(" %s %s", r.password, cmd)...)
|
r.buf.Write(r.Header())
|
||||||
return datagram
|
r.buf.WriteString(fmt.Sprintf(" %s %s", r.password, cmd))
|
||||||
|
return r.buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|||||||
27
option.go
Normal file
27
option.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package q3rcon
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Option is a functional option type that allows us to configure the VbanTxt.
|
||||||
|
type Option func(*Rcon)
|
||||||
|
|
||||||
|
// WithLoginTimeout is a functional option to set the login timeout
|
||||||
|
func WithLoginTimeout(timeout time.Duration) Option {
|
||||||
|
return func(rcon *Rcon) {
|
||||||
|
rcon.loginTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaultTimeout is a functional option to set the default response timeout
|
||||||
|
func WithDefaultTimeout(timeout time.Duration) Option {
|
||||||
|
return func(rcon *Rcon) {
|
||||||
|
rcon.defaultTimeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeouts is a functional option to set the timeouts for responses per command
|
||||||
|
func WithTimeouts(timeouts map[string]time.Duration) Option {
|
||||||
|
return func(rcon *Rcon) {
|
||||||
|
rcon.timeouts = timeouts
|
||||||
|
}
|
||||||
|
}
|
||||||
87
q3rcon.go
87
q3rcon.go
@@ -1,39 +1,28 @@
|
|||||||
package q3rcon
|
package q3rcon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/conn"
|
"github.com/onyx-and-iris/q3rcon/internal/conn"
|
||||||
"github.com/onyx-and-iris/q3rcon/internal/packet"
|
"github.com/onyx-and-iris/q3rcon/internal/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Option is a functional option type that allows us to configure the VbanTxt.
|
const respBufSiz = 2048
|
||||||
type Option func(*Rcon)
|
|
||||||
|
|
||||||
func WithDefaultTimeout(timeout time.Duration) Option {
|
|
||||||
return func(rcon *Rcon) {
|
|
||||||
rcon.defaultTimeout = timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimeouts is a functional option to set the timeouts for responses
|
|
||||||
func WithTimeouts(timeouts map[string]time.Duration) Option {
|
|
||||||
return func(rcon *Rcon) {
|
|
||||||
rcon.timeouts = timeouts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rcon struct {
|
type Rcon struct {
|
||||||
conn conn.UDPConn
|
conn conn.UDPConn
|
||||||
request packet.Request
|
request packet.Request
|
||||||
response packet.Response
|
response packet.Response
|
||||||
|
|
||||||
|
loginTimeout time.Duration
|
||||||
defaultTimeout time.Duration
|
defaultTimeout time.Duration
|
||||||
timeouts map[string]time.Duration
|
timeouts map[string]time.Duration
|
||||||
|
|
||||||
resp chan string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(host string, port int, password string, options ...Option) (*Rcon, error) {
|
func New(host string, port int, password string, options ...Option) (*Rcon, error) {
|
||||||
@@ -49,7 +38,9 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
|
|||||||
r := &Rcon{
|
r := &Rcon{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
request: packet.NewRequest(password),
|
request: packet.NewRequest(password),
|
||||||
resp: make(chan string),
|
response: packet.NewResponse(),
|
||||||
|
|
||||||
|
loginTimeout: 5 * time.Second,
|
||||||
defaultTimeout: 20 * time.Millisecond,
|
defaultTimeout: 20 * time.Millisecond,
|
||||||
timeouts: make(map[string]time.Duration),
|
timeouts: make(map[string]time.Duration),
|
||||||
}
|
}
|
||||||
@@ -58,16 +49,15 @@ func New(host string, port int, password string, options ...Option) (*Rcon, erro
|
|||||||
o(r)
|
o(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.Login()
|
if err = r.login(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Rcon) Login() error {
|
func (r Rcon) login() error {
|
||||||
timeout := time.After(2 * time.Second)
|
timeout := time.After(r.loginTimeout)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
@@ -90,24 +80,63 @@ func (r Rcon) Login() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Rcon) Send(cmd string) (string, error) {
|
func (r Rcon) Send(cmdWithArgs string) (string, error) {
|
||||||
|
cmd, _, _ := strings.Cut(string(cmdWithArgs), " ")
|
||||||
timeout, ok := r.timeouts[cmd]
|
timeout, ok := r.timeouts[cmd]
|
||||||
if !ok {
|
if !ok {
|
||||||
timeout = r.defaultTimeout
|
timeout = r.defaultTimeout
|
||||||
|
} else {
|
||||||
|
log.Debugf("%s in timeouts map, using timeout %v", cmd, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
e := make(chan error)
|
respChan := make(chan string)
|
||||||
go r.conn.Listen(timeout, r.resp, e)
|
errChan := make(chan error)
|
||||||
_, err := r.conn.Write(r.request.Encode(cmd))
|
|
||||||
|
go r.listen(timeout, respChan, errChan)
|
||||||
|
|
||||||
|
_, err := r.conn.Write(r.request.Encode(cmdWithArgs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-e:
|
case err := <-errChan:
|
||||||
return "", err
|
return "", err
|
||||||
case resp := <-r.resp:
|
case resp := <-respChan:
|
||||||
return strings.TrimPrefix(resp, string(r.response.Header())), nil
|
return resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan<- error) {
|
||||||
|
done := make(chan struct{})
|
||||||
|
respBuf := make([]byte, respBufSiz)
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
respChan <- sb.String()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
rlen, err := r.conn.ReadUntil(time.Now().Add(timeout), respBuf)
|
||||||
|
if err != nil {
|
||||||
|
e, ok := err.(net.Error)
|
||||||
|
if ok {
|
||||||
|
if e.Timeout() {
|
||||||
|
close(done)
|
||||||
|
} else {
|
||||||
|
errChan <- e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rlen > len(r.response.Header()) {
|
||||||
|
if bytes.HasPrefix(respBuf, r.response.Header()) {
|
||||||
|
sb.Write(respBuf[len(r.response.Header()):rlen])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user