mirror of
https://github.com/onyx-and-iris/q3rcon.git
synced 2026-03-03 09:29:35 +00:00
Compare commits
No commits in common. "b1161e1e9734f5e9f3817a7aa478628b3e6111e7" and "caffd65cb3b7cf6707b4daca885e967c19e3adce" have entirely different histories.
b1161e1e97
...
caffd65cb3
@ -205,7 +205,6 @@ func interactiveMode(client *q3rcon.Rcon, input io.Reader) error {
|
|||||||
resp, err := client.Send(cmd)
|
resp, err := client.Send(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
fmt.Print(">> ")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("%s>> ", removeColourCodes(resp))
|
fmt.Printf("%s>> ", removeColourCodes(resp))
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
package q3rcon
|
package conn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/log"
|
"github.com/charmbracelet/log"
|
||||||
)
|
)
|
||||||
@ -11,23 +12,23 @@ type UDPConn struct {
|
|||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUDPConn(host string, port int) (*UDPConn, error) {
|
func New(host string, port int) (UDPConn, error) {
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp4", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
|
udpAddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", host, port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return UDPConn{}, err
|
||||||
}
|
}
|
||||||
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
conn, err := net.DialUDP("udp4", nil, udpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return UDPConn{}, 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
|
||||||
@ -36,7 +37,8 @@ func (c *UDPConn) Write(buf []byte) (int, error) {
|
|||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UDPConn) Read(buf []byte) (int, error) {
|
func (c UDPConn) ReadUntil(timeout time.Time, 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
|
||||||
@ -44,7 +46,7 @@ func (c *UDPConn) Read(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
|
||||||
36
internal/packet/request.go
Normal file
36
internal/packet/request.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
13
internal/packet/response.go
Normal file
13
internal/packet/response.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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,31 +1,24 @@
|
|||||||
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 io.ReadWriteCloser
|
conn conn.UDPConn
|
||||||
request encoder
|
request packet.Request
|
||||||
response decoder
|
response packet.Response
|
||||||
|
|
||||||
loginTimeout time.Duration
|
loginTimeout time.Duration
|
||||||
defaultTimeout time.Duration
|
defaultTimeout time.Duration
|
||||||
@ -37,15 +30,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 := newUDPConn(host, port)
|
conn, err := conn.New(host, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating UDP connection: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &Rcon{
|
r := &Rcon{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
request: newRequest(password),
|
request: packet.NewRequest(password),
|
||||||
response: newResponse(),
|
response: packet.NewResponse(),
|
||||||
|
|
||||||
loginTimeout: 5 * time.Second,
|
loginTimeout: 5 * time.Second,
|
||||||
defaultTimeout: 20 * time.Millisecond,
|
defaultTimeout: 20 * time.Millisecond,
|
||||||
@ -57,7 +50,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, fmt.Errorf("error logging in: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
@ -72,7 +65,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 fmt.Errorf("error sending login command: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
if resp == "" {
|
if resp == "" {
|
||||||
continue
|
continue
|
||||||
@ -101,14 +94,9 @@ func (r Rcon) Send(cmdWithArgs string) (string, error) {
|
|||||||
|
|
||||||
go r.listen(timeout, respChan, errChan)
|
go r.listen(timeout, respChan, errChan)
|
||||||
|
|
||||||
encodedCmd, err := r.request.encode(cmdWithArgs)
|
_, err := r.conn.Write(r.request.Encode(cmdWithArgs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error encoding command: %w", err)
|
return "", err
|
||||||
}
|
|
||||||
|
|
||||||
_, err = r.conn.Write(encodedCmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error writing command to connection: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -130,17 +118,7 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
|
|||||||
respChan <- sb.String()
|
respChan <- sb.String()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
c, ok := r.conn.(*UDPConn)
|
rlen, err := r.conn.ReadUntil(time.Now().Add(timeout), respBuf)
|
||||||
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 {
|
||||||
@ -153,8 +131,10 @@ func (r Rcon) listen(timeout time.Duration, respChan chan<- string, errChan chan
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.response.isValid(respBuf[:rlen]) {
|
if rlen > len(r.response.Header()) {
|
||||||
sb.WriteString(r.response.decode(respBuf[:rlen]))
|
if bytes.HasPrefix(respBuf, r.response.Header()) {
|
||||||
|
sb.Write(respBuf[len(r.response.Header()):rlen])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
request.go
35
request.go
@ -1,35 +0,0 @@
|
|||||||
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
21
response.go
@ -1,21 +0,0 @@
|
|||||||
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