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