diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-06-20 09:40:07 -0700 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-06-20 09:40:07 -0700 |
| commit | fb653292612eddca5b088ed88466c546d1597107 (patch) | |
| tree | 900ed32e066812688858ed3bb15128c41bb78054 /adapters/rcon/rcon.go | |
| download | penguins.lan-fb653292612eddca5b088ed88466c546d1597107.tar.gz penguins.lan-fb653292612eddca5b088ed88466c546d1597107.zip | |
Initial commit
Diffstat (limited to 'adapters/rcon/rcon.go')
| -rw-r--r-- | adapters/rcon/rcon.go | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/adapters/rcon/rcon.go b/adapters/rcon/rcon.go new file mode 100644 index 0000000..4b9e8b6 --- /dev/null +++ b/adapters/rcon/rcon.go @@ -0,0 +1,130 @@ +// Package rcon is a minimal Source RCON client (the protocol Minecraft speaks), +// enough to authenticate and run a command like "list". +package rcon + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net" + "time" +) + +const ( + typeResponse int32 = 0 + typeAuthResp int32 = 2 + typeExecCommand int32 = 2 + typeAuth int32 = 3 + + authRequestID int32 = 0 + execRequestID int32 = 1 + + headerSize = 8 // id (int32) + type (int32) + paddingSize = 2 // two trailing null bytes + maxBodySize = 4096 + + DefaultTimeout = 5 * time.Second +) + +var ( + ErrAuthFailed = errors.New("rcon: authentication failed") + ErrResponseMismatch = errors.New("rcon: response id mismatch") + ErrPacketSize = errors.New("rcon: invalid packet size") +) + +type Client struct { + conn net.Conn + timeout time.Duration +} + +func Dial(address, password string, timeout time.Duration) (*Client, error) { + conn, err := net.DialTimeout("tcp", address, timeout) + if err != nil { + return nil, err + } + + client := &Client{conn: conn, timeout: timeout} + if err := client.auth(password); err != nil { + conn.Close() + return nil, err + } + return client, nil +} + +func (c *Client) Close() error { return c.conn.Close() } + +func (c *Client) auth(password string) error { + if err := c.write(authRequestID, typeAuth, password); err != nil { + return err + } + + // Some servers send an empty SERVERDATA_RESPONSE_VALUE before the auth + // response; skip anything that isn't the auth response. + for { + id, typ, _, err := c.read() + if err != nil { + return err + } + if typ != typeAuthResp { + continue + } + if id == -1 { + return ErrAuthFailed + } + return nil + } +} + +func (c *Client) Execute(command string) (string, error) { + if err := c.write(execRequestID, typeExecCommand, command); err != nil { + return "", err + } + + id, _, body, err := c.read() + if err != nil { + return "", err + } + if id != execRequestID { + return "", ErrResponseMismatch + } + return body, nil +} + +func (c *Client) write(id, typ int32, body string) error { + c.conn.SetWriteDeadline(time.Now().Add(c.timeout)) + + var buf bytes.Buffer + size := int32(headerSize + len(body) + paddingSize) + binary.Write(&buf, binary.LittleEndian, size) + binary.Write(&buf, binary.LittleEndian, id) + binary.Write(&buf, binary.LittleEndian, typ) + buf.WriteString(body) + buf.Write([]byte{0, 0}) + + _, err := c.conn.Write(buf.Bytes()) + return err +} + +func (c *Client) read() (id, typ int32, body string, err error) { + c.conn.SetReadDeadline(time.Now().Add(c.timeout)) + + var size int32 + if err = binary.Read(c.conn, binary.LittleEndian, &size); err != nil { + return + } + if size < headerSize+paddingSize || size > headerSize+maxBodySize+paddingSize { + err = ErrPacketSize + return + } + + payload := make([]byte, size) + if _, err = io.ReadFull(c.conn, payload); err != nil { + return + } + + id = int32(binary.LittleEndian.Uint32(payload[0:4])) + typ = int32(binary.LittleEndian.Uint32(payload[4:8])) + body = string(payload[headerSize : size-paddingSize]) + return +} |
