Compare commits

..

No commits in common. "b1161e1e9734f5e9f3817a7aa478628b3e6111e7" and "caffd65cb3b7cf6707b4daca885e967c19e3adce" have entirely different histories.

7 changed files with 80 additions and 106 deletions

View File

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

View File

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

View 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()
}

View 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"...)
}

View File

@ -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])
}
} }
} }
} }

View File

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

View File

@ -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):])
}