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