CS2 tags a command's output with the id of the empty end packet when both arrive together, and replies to the end packet with a bare \x00\x01. The client dropped the output as stale, so the panel reported WebPanelBridge as missing. Also log each new kind of poll failure once, and recovery.
158 lines
4.1 KiB
Go
158 lines
4.1 KiB
Go
package rcon
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestArg(t *testing.T) {
|
|
cases := map[string]string{
|
|
`plain reason`: "plain reason",
|
|
`quote" ; quit`: "quote quit",
|
|
"new\nline": "new line",
|
|
`back\slash`: "backslash",
|
|
" padded ": "padded",
|
|
`"; css_addadmin 1 x @css/root`: " css_addadmin 1 x @css/root",
|
|
}
|
|
for in, want := range cases {
|
|
if got := Arg(in, 0); got != strings.TrimSpace(want) {
|
|
t.Errorf("Arg(%q) = %q, want %q", in, got, strings.TrimSpace(want))
|
|
}
|
|
}
|
|
if got := Arg("abcdef", 3); got != "abc" {
|
|
t.Errorf("max length: got %q", got)
|
|
}
|
|
}
|
|
|
|
// fakeServer answers auth, then replies to each command with its text split over two packets,
|
|
// and mirrors the empty end-marker packet the way srcds does.
|
|
func fakeServer(t *testing.T, password string, cs2 bool) string {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { ln.Close() })
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go serve(c, password, cs2)
|
|
}
|
|
}()
|
|
return ln.Addr().String()
|
|
}
|
|
|
|
func readPkt(r io.Reader) (int32, int32, string, error) {
|
|
var size int32
|
|
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
|
|
return 0, 0, "", err
|
|
}
|
|
b := make([]byte, size)
|
|
if _, err := io.ReadFull(r, b); err != nil {
|
|
return 0, 0, "", err
|
|
}
|
|
return int32(binary.LittleEndian.Uint32(b)), int32(binary.LittleEndian.Uint32(b[4:])), strings.TrimRight(string(b[8:]), "\x00"), nil
|
|
}
|
|
|
|
func writePkt(w io.Writer, id, typ int32, body string) {
|
|
buf := make([]byte, 12+len(body)+2)
|
|
binary.LittleEndian.PutUint32(buf, uint32(len(body)+10))
|
|
binary.LittleEndian.PutUint32(buf[4:], uint32(id))
|
|
binary.LittleEndian.PutUint32(buf[8:], uint32(typ))
|
|
copy(buf[12:], body)
|
|
w.Write(buf)
|
|
}
|
|
|
|
func serve(c net.Conn, password string, cs2 bool) {
|
|
defer c.Close()
|
|
pending := ""
|
|
for {
|
|
id, typ, body, err := readPkt(c)
|
|
if err != nil {
|
|
return
|
|
}
|
|
switch typ {
|
|
case typeAuth:
|
|
writePkt(c, id, typeResponseValue, "")
|
|
if body == password {
|
|
writePkt(c, id, typeAuthResponse, "")
|
|
} else {
|
|
writePkt(c, -1, typeAuthResponse, "")
|
|
}
|
|
case typeExecCommand:
|
|
if cs2 {
|
|
// CS2 answers after reading the end packet too, tagging the output with its id.
|
|
pending = "echo:" + body + "\n"
|
|
continue
|
|
}
|
|
out := "echo:" + body + "\n" + strings.Repeat("x", 5000)
|
|
writePkt(c, id, typeResponseValue, out[:4000])
|
|
writePkt(c, id, typeResponseValue, out[4000:])
|
|
case typeResponseValue:
|
|
if cs2 {
|
|
writePkt(c, id, typeResponseValue, pending)
|
|
writePkt(c, id, typeResponseValue, "\x00\x01")
|
|
continue
|
|
}
|
|
writePkt(c, id, typeResponseValue, "")
|
|
writePkt(c, id, typeResponseValue, "\x00\x01\x00\x00")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecMultiPacket(t *testing.T) {
|
|
addr := fakeServer(t, "pw", false)
|
|
c := New(addr, "pw")
|
|
defer c.Close()
|
|
for i := 0; i < 3; i++ {
|
|
out, err := c.Exec("status")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(out, "echo:status\n") || len(out) != len("echo:status\n")+5000 {
|
|
t.Fatalf("got %d bytes: %.40q", len(out), out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWrongPassword(t *testing.T) {
|
|
addr := fakeServer(t, "pw", false)
|
|
c := New(addr, "nope")
|
|
if _, err := c.Exec("status"); err != ErrAuth {
|
|
t.Fatalf("got %v, want ErrAuth", err)
|
|
}
|
|
}
|
|
|
|
func TestBackoff(t *testing.T) {
|
|
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
addr := ln.Addr().String()
|
|
ln.Close() // nothing listens here now
|
|
c := New(addr, "pw")
|
|
if _, err := c.Exec("status"); err == nil {
|
|
t.Fatal("expected a connection error")
|
|
}
|
|
if _, err := c.Exec("status"); !errors.Is(err, ErrDown) {
|
|
t.Fatalf("second call should fail fast with ErrDown, got %v", err)
|
|
}
|
|
}
|
|
|
|
// CS2 tags output with the end packet's id and ends with a bare "\x00\x01" (seen live on fr04).
|
|
func TestExecCS2Ids(t *testing.T) {
|
|
c := New(fakeServer(t, "pw", true), "pw")
|
|
defer c.Close()
|
|
for i := 0; i < 3; i++ {
|
|
out, err := c.Exec("css_webpanel_status")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out != "echo:css_webpanel_status\n" {
|
|
t.Fatalf("got %q", out)
|
|
}
|
|
}
|
|
}
|