summaryrefslogtreecommitdiff
path: root/adapters/rcon/rcon.go
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/rcon/rcon.go')
-rw-r--r--adapters/rcon/rcon.go130
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
+}