package rcon import "time" // Settings contains option to Conn. type Settings struct { dialTimeout time.Duration deadline time.Duration maxCommandLen int } // DefaultSettings provides default deadline settings to Conn. var DefaultSettings = Settings{ dialTimeout: DefaultDialTimeout, deadline: DefaultDeadline, maxCommandLen: DefaultMaxCommandLen, } // Option allows to inject settings to Settings. type Option func(s *Settings) // SetDialTimeout injects dial Timeout to Settings. func SetDialTimeout(timeout time.Duration) Option { return func(s *Settings) { s.dialTimeout = timeout } } // SetDeadline injects read/write Timeout to Settings. func SetDeadline(timeout time.Duration) Option { return func(s *Settings) { s.deadline = timeout } } // SetMaxCommandLen injects maxCommandLen to Settings. func SetMaxCommandLen(maxCommandLen int) Option { return func(s *Settings) { s.maxCommandLen = maxCommandLen } }
package rcon import ( "bytes" "encoding/binary" "fmt" "io" ) // Packet sizes definitions. const ( PacketPaddingSize int32 = 2 // Size of Packet's padding. PacketHeaderSize int32 = 8 // Size of Packet's header. MinPacketSize = PacketPaddingSize + PacketHeaderSize MaxPacketSize = 4096 + MinPacketSize ) // Packet is a rcon packet. Both requests and responses are sent as // TCP packets. Their payload follows the following basic structure. type Packet struct { // The packet size field is a 32-bit little endian integer, representing // the length of the request in bytes. Note that the packet size field // itself is not included when determining the size of the packet, // so the value of this field is always 4 less than the packet's actual // length. The minimum possible value for packet size is 10. // The maximum possible value of packet size is 4096. // If the response is too large to fit into one packet, it will be split // and sent as multiple packets. Size int32 // The packet id field is a 32-bit little endian integer chosen by the // client for each request. It may be set to any positive integer. // When the RemoteServer responds to the request, the response packet // will have the same packet id as the original request (unless it is // a failed SERVERDATA_AUTH_RESPONSE packet). // It need not be unique, but if a unique packet id is assigned, // it can be used to match incoming responses to their corresponding requests. ID int32 // The packet type field is a 32-bit little endian integer, which indicates // the purpose of the packet. Its value will always be either 0, 2, or 3, // depending on which of the following request/response types the packet // represents: // SERVERDATA_AUTH = 3, // SERVERDATA_AUTH_RESPONSE = 2, // SERVERDATA_EXECCOMMAND = 2, // SERVERDATA_RESPONSE_VALUE = 0. Type int32 // The packet body field is a null-terminated string encoded in ASCII // (i.e. ASCIIZ). Depending on the packet type, it may contain either the // RCON MockPassword for the RemoteServer, the command to be executed, // or the RemoteServer's response to a request. body []byte } // NewPacket creates and initializes a new Packet using packetType, // packetID and body as its initial contents. NewPacket is intended to // calculate packet size from body length and 10 bytes for rcon headers // and termination strings. func NewPacket(packetType int32, packetID int32, body string) *Packet { size := len([]byte(body)) + int(PacketHeaderSize+PacketPaddingSize) return &Packet{ Size: int32(size), //nolint:gosec // No matter Type: packetType, ID: packetID, body: []byte(body), } } // Body returns packet bytes body as a string. func (packet *Packet) Body() string { return string(packet.body) } // WriteTo implements io.WriterTo for write a packet to w. func (packet *Packet) WriteTo(w io.Writer) (int64, error) { buffer := bytes.NewBuffer(make([]byte, 0, packet.Size+4)) _ = binary.Write(buffer, binary.LittleEndian, packet.Size) _ = binary.Write(buffer, binary.LittleEndian, packet.ID) _ = binary.Write(buffer, binary.LittleEndian, packet.Type) // Write command body, null terminated ASCII string and an empty ASCIIZ string. buffer.Write(append(packet.body, 0x00, 0x00)) return buffer.WriteTo(w) } // ReadFrom implements io.ReaderFrom for read a packet from r. func (packet *Packet) ReadFrom(r io.Reader) (int64, error) { var n int64 if err := binary.Read(r, binary.LittleEndian, &packet.Size); err != nil { return n, fmt.Errorf("rcon: read packet size: %w", err) } n += 4 if packet.Size < MinPacketSize { return n, ErrResponseTooSmall } if err := binary.Read(r, binary.LittleEndian, &packet.ID); err != nil { return n, fmt.Errorf("rcon: read packet id: %w", err) } n += 4 if err := binary.Read(r, binary.LittleEndian, &packet.Type); err != nil { return n, fmt.Errorf("rcon: read packet type: %w", err) } n += 4 // String can actually include null characters which is the case in // response to a SERVERDATA_RESPONSE_VALUE packet. packet.body = make([]byte, packet.Size-PacketHeaderSize) var i int64 for i < int64(packet.Size-PacketHeaderSize) { var m int var err error if m, err = r.Read(packet.body[i:]); err != nil { return n + int64(m) + i, fmt.Errorf("rcon: %w", err) } i += int64(m) } n += i // Remove null terminated strings from response body. if !bytes.Equal(packet.body[len(packet.body)-int(PacketPaddingSize):], []byte{0x00, 0x00}) { return n, ErrInvalidPacketPadding } packet.body = packet.body[0 : len(packet.body)-int(PacketPaddingSize)] return n, nil }
// Package rcon implements Source RCON Protocol which is described in the // documentation: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol. package rcon import ( "encoding/binary" "errors" "fmt" "net" "time" ) const ( // DefaultDialTimeout provides default auth timeout to remote server. DefaultDialTimeout = 5 * time.Second // DefaultDeadline provides default deadline to tcp read/write operations. DefaultDeadline = 5 * time.Second // DefaultMaxCommandLen is an artificial restriction, but it will help in case of random // large queries. DefaultMaxCommandLen = 1000 // SERVERDATA_AUTH is the first packet sent by the client, // which is used to authenticate the conn with the server. SERVERDATA_AUTH int32 = 3 // SERVERDATA_AUTH_ID is any positive integer, chosen by the client // (will be mirrored back in the server's response). SERVERDATA_AUTH_ID int32 = 0 // SERVERDATA_AUTH_RESPONSE packet is a notification of the conn's current auth // status. When the server receives an auth request, it will respond with an empty // SERVERDATA_RESPONSE_VALUE, followed immediately by a SERVERDATA_AUTH_RESPONSE // indicating whether authentication succeeded or failed. Note that the status // code is returned in the packet id field, so when pairing the response with // the original auth request, you may need to look at the packet id of the // preceding SERVERDATA_RESPONSE_VALUE. // If authentication was successful, the ID assigned by the request. // If auth failed, -1 (0xFF FF FF FF). SERVERDATA_AUTH_RESPONSE int32 = 2 // SERVERDATA_RESPONSE_VALUE packet is the response to a SERVERDATA_EXECCOMMAND // request. The ID assigned by the original request. SERVERDATA_RESPONSE_VALUE int32 = 0 // SERVERDATA_EXECCOMMAND packet type represents a command issued to the server // by a client. The response will vary depending on the command issued. SERVERDATA_EXECCOMMAND int32 = 2 // SERVERDATA_EXECCOMMAND_ID is any positive integer, chosen by the client // (will be mirrored back in the server's response). SERVERDATA_EXECCOMMAND_ID int32 = 0 ) var ( // ErrAuthNotRCON is returned when got auth response with negative size. ErrAuthNotRCON = errors.New("response from not rcon server") // ErrInvalidAuthResponse is returned when we didn't get an auth packet // back for second read try after discard empty SERVERDATA_RESPONSE_VALUE // from authentication response. ErrInvalidAuthResponse = errors.New("invalid authentication packet type response") // ErrAuthFailed is returned when the package id from authentication // response is -1. ErrAuthFailed = errors.New("authentication failed") // ErrInvalidPacketID is returned when the package id from server response // was not mirrored back from request. ErrInvalidPacketID = errors.New("response for another request") // ErrInvalidPacketPadding is returned when the bytes after type field from // response is not equal to null-terminated ASCII strings. ErrInvalidPacketPadding = errors.New("invalid response padding") // ErrResponseTooSmall is returned when the server response is smaller // than 10 bytes. ErrResponseTooSmall = errors.New("response too small") // ErrCommandTooLong is returned when executed command length is bigger // than MaxCommandLen characters. ErrCommandTooLong = errors.New("command too long") // ErrCommandEmpty is returned when executed command length equal 0. ErrCommandEmpty = errors.New("command too small") // ErrMultiErrorOccurred is returned when close connection failed with // error after auth failed. ErrMultiErrorOccurred = errors.New("an error occurred while handling another error") ) // Conn is source RCON generic stream-oriented network connection. type Conn struct { conn net.Conn settings Settings } // open creates a new Conn from an existing net.Conn and authenticates it. func open(conn net.Conn, password string, settings Settings) (*Conn, error) { client := Conn{conn: conn, settings: settings} if err := client.auth(password); err != nil { // Failed to auth conn with the server. if err2 := client.Close(); err2 != nil { return &client, fmt.Errorf("%w: %s. Previous error: %s", ErrMultiErrorOccurred, err2.Error(), err.Error()) } return &client, fmt.Errorf("rcon: %w", err) } return &client, nil } // Open creates a new authorized Conn from an existing net.Conn. func Open(conn net.Conn, password string, options ...Option) (*Conn, error) { settings := DefaultSettings for _, option := range options { option(&settings) } return open(conn, password, settings) } // Dial creates a new authorized Conn tcp dialer connection. func Dial(address string, password string, options ...Option) (*Conn, error) { settings := DefaultSettings for _, option := range options { option(&settings) } conn, err := net.DialTimeout("tcp", address, settings.dialTimeout) if err != nil { // Failed to open TCP connection to the server. return nil, fmt.Errorf("rcon: %w", err) } return open(conn, password, settings) } // Execute sends command type and it string to execute to the remote server, // creating a packet with a SERVERDATA_EXECCOMMAND_ID for the server to mirror, // and compiling its payload bytes in the appropriate order. The response body // is decompiled from bytes into a string for return. func (c *Conn) Execute(command string) (string, error) { if command == "" { return "", ErrCommandEmpty } if c.settings.maxCommandLen > 0 && len(command) > c.settings.maxCommandLen { return "", ErrCommandTooLong } if err := c.write(SERVERDATA_EXECCOMMAND, SERVERDATA_EXECCOMMAND_ID, command); err != nil { return "", err } response, err := c.read() if err != nil { return response.Body(), err } if response.ID != SERVERDATA_EXECCOMMAND_ID { return response.Body(), ErrInvalidPacketID } return response.Body(), nil } // LocalAddr returns the local network address. func (c *Conn) LocalAddr() net.Addr { return c.conn.LocalAddr() } // RemoteAddr returns the remote network address. func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() } // Close closes the connection. func (c *Conn) Close() error { return c.conn.Close() } // auth sends SERVERDATA_AUTH request to the remote server and // authenticates client for the next requests. func (c *Conn) auth(password string) error { if err := c.write(SERVERDATA_AUTH, SERVERDATA_AUTH_ID, password); err != nil { return err } if c.settings.deadline != 0 { if err := c.conn.SetReadDeadline(time.Now().Add(c.settings.deadline)); err != nil { return fmt.Errorf("rcon: %w", err) } } response, err := c.readHeader() if err != nil { return err } size := response.Size - PacketHeaderSize if size < 0 { return ErrAuthNotRCON } // When the server receives an auth request, it will respond with an empty // SERVERDATA_RESPONSE_VALUE, followed immediately by a SERVERDATA_AUTH_RESPONSE // indicating whether authentication succeeded or failed. // Some servers doesn't send an empty SERVERDATA_RESPONSE_VALUE packet, so we // do this case optional. if response.Type == SERVERDATA_RESPONSE_VALUE { // Discard empty SERVERDATA_RESPONSE_VALUE from authentication response. _, _ = c.conn.Read(make([]byte, size)) if response, err = c.readHeader(); err != nil { return err } } // We must to read response body. buffer := make([]byte, size) if _, err := c.conn.Read(buffer); err != nil { return fmt.Errorf("rcon: %w", err) } if response.Type != SERVERDATA_AUTH_RESPONSE { return ErrInvalidAuthResponse } if response.ID == -1 { return ErrAuthFailed } if response.ID != SERVERDATA_AUTH_ID { return ErrInvalidPacketID } return nil } // write creates packet and writes it to established tcp conn. func (c *Conn) write(packetType int32, packetID int32, command string) error { if c.settings.deadline != 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.settings.deadline)); err != nil { return fmt.Errorf("rcon: %w", err) } } packet := NewPacket(packetType, packetID, command) _, err := packet.WriteTo(c.conn) return err } // read reads structured binary data from c.conn into packet. func (c *Conn) read() (*Packet, error) { if c.settings.deadline != 0 { if err := c.conn.SetReadDeadline(time.Now().Add(c.settings.deadline)); err != nil { return nil, fmt.Errorf("rcon: %w", err) } } packet := &Packet{} if _, err := packet.ReadFrom(c.conn); err != nil { return packet, err } // Workaround for Rust server. // Rust rcon server responses packet with a type of 4 and the next packet // is valid. It is undocumented, so skip packet and read next. if packet.Type == 4 { if _, err := packet.ReadFrom(c.conn); err != nil { return packet, err } // One more workaround for Rust server. // When sent command "Say" there is no response data from server with // packet.ID = SERVERDATA_EXECCOMMAND_ID, only previous console message // that command was received with packet.ID = -1, therefore, forcibly // set packet.ID to SERVERDATA_EXECCOMMAND_ID. if packet.ID == -1 { packet.ID = SERVERDATA_EXECCOMMAND_ID } } return packet, nil } // readHeader reads structured binary data without body from c.conn into packet. func (c *Conn) readHeader() (Packet, error) { var packet Packet if err := binary.Read(c.conn, binary.LittleEndian, &packet.Size); err != nil { return packet, fmt.Errorf("rcon: read packet size: %w", err) } if err := binary.Read(c.conn, binary.LittleEndian, &packet.ID); err != nil { return packet, fmt.Errorf("rcon: read packet id: %w", err) } if err := binary.Read(c.conn, binary.LittleEndian, &packet.Type); err != nil { return packet, fmt.Errorf("rcon: read packet type: %w", err) } return packet, nil }
package rcontest import ( "net" "github.com/gorcon/rcon" ) // Context represents the context of the current RCON request. It holds request // and conn objects. type Context struct { server *Server conn net.Conn request *rcon.Packet } // Server returns the Server instance. func (c *Context) Server() *Server { return c.server } // Conn returns current RCON connection. func (c *Context) Conn() net.Conn { return c.conn } // Request returns received *rcon.Packet. func (c *Context) Request() *rcon.Packet { return c.request }
package rcontest // Option allows to inject Settings to Server. type Option func(s *Server) // SetSettings injects configuration for RCON Server. func SetSettings(settings Settings) Option { return func(s *Server) { s.Settings = settings } } // SetAuthHandler injects HandlerFunc with authorisation data checking. func SetAuthHandler(handler HandlerFunc) Option { return func(s *Server) { s.SetAuthHandler(handler) } } // SetCommandHandler injects HandlerFunc with commands processing. func SetCommandHandler(handler HandlerFunc) Option { return func(s *Server) { s.SetCommandHandler(handler) } }
// Package rcontest contains RCON server for RCON client testing. package rcontest import ( "errors" "fmt" "io" "net" "sync" "time" "github.com/gorcon/rcon" ) // Server is an RCON server listening on a system-chosen port on the // local loopback interface, for use in end-to-end RCON tests. type Server struct { Settings Settings Listener net.Listener addr string authHandler HandlerFunc commandHandler HandlerFunc connections map[net.Conn]struct{} quit chan bool wg sync.WaitGroup mu sync.Mutex closed bool } // Settings contains configuration for RCON Server. type Settings struct { Password string AuthResponseDelay time.Duration CommandResponseDelay time.Duration } // HandlerFunc defines a function to serve RCON requests. type HandlerFunc func(c *Context) // AuthHandler checks authorisation data and responses with // SERVERDATA_AUTH_RESPONSE packet. func AuthHandler(c *Context) { if c.Request().Body() == c.Server().Settings.Password { // First write SERVERDATA_RESPONSE_VALUE packet with empty body. _, _ = rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, c.Request().ID, "").WriteTo(c.Conn()) // Than write SERVERDATA_AUTH_RESPONSE packet to allow authHandler success. _, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, rcon.SERVERDATA_AUTH_ID, "").WriteTo(c.Conn()) } else { // If authentication was failed, the ID must be assigned to -1. _, _ = rcon.NewPacket(rcon.SERVERDATA_AUTH_RESPONSE, -1, string([]byte{0x00})).WriteTo(c.Conn()) } } // EmptyHandler responses with empty body. Is used when start RCON Server with nil // commandHandler. func EmptyHandler(c *Context) { _, _ = rcon.NewPacket(rcon.SERVERDATA_RESPONSE_VALUE, c.Request().ID, "").WriteTo(c.Conn()) } func newLocalListener() net.Listener { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { panic(fmt.Sprintf("rcontest: failed to listen on a port: %v", err)) } return l } // NewServer returns a running RCON Server or nil if an error occurred. // The caller should call Close when finished, to shut it down. func NewServer(options ...Option) *Server { server := NewUnstartedServer(options...) server.Start() return server } // NewUnstartedServer returns a new Server but doesn't start it. // After changing its configuration, the caller should call Start. // The caller should call Close when finished, to shut it down. func NewUnstartedServer(options ...Option) *Server { server := Server{ Listener: newLocalListener(), authHandler: AuthHandler, commandHandler: EmptyHandler, connections: make(map[net.Conn]struct{}), quit: make(chan bool), } for _, option := range options { option(&server) } return &server } // SetAuthHandler injects HandlerFunc with authorisation data checking. func (s *Server) SetAuthHandler(handler HandlerFunc) { s.authHandler = handler } // SetCommandHandler injects HandlerFunc with commands processing. func (s *Server) SetCommandHandler(handler HandlerFunc) { s.commandHandler = handler } // Start starts a server from NewUnstartedServer. func (s *Server) Start() { if s.addr != "" { panic("server already started") } s.addr = s.Listener.Addr().String() s.goServe() } // Close shuts down the Server. func (s *Server) Close() { if s.closed { return } s.closed = true close(s.quit) s.Listener.Close() // Waiting for server connections. s.wg.Wait() s.mu.Lock() for c := range s.connections { // Force-close any connections. s.closeConn(c) } s.mu.Unlock() } // Addr returns IPv4 string Server address. func (s *Server) Addr() string { return s.addr } // NewContext returns a Context instance. func (s *Server) NewContext(conn net.Conn) (*Context, error) { ctx := Context{server: s, conn: conn, request: &rcon.Packet{}} if _, err := ctx.request.ReadFrom(conn); err != nil { return &ctx, fmt.Errorf("rcontest: %w", err) } return &ctx, nil } // serve handles incoming requests until a stop signal is given with Close. func (s *Server) serve() { for { conn, err := s.Listener.Accept() if err != nil { if s.isRunning() { panic(fmt.Errorf("rcontest: %w", err)) } return } s.wg.Add(1) go s.handle(conn) } } // serve calls serve in goroutine. func (s *Server) goServe() { s.wg.Add(1) go func() { defer s.wg.Done() s.serve() }() } // handle handles incoming client conn. func (s *Server) handle(conn net.Conn) { s.mu.Lock() s.connections[conn] = struct{}{} s.mu.Unlock() defer func() { s.closeConn(conn) s.wg.Done() }() for { ctx, err := s.NewContext(conn) if err != nil { if !errors.Is(err, io.EOF) { panic(fmt.Errorf("failed read request: %w", err)) } return } switch ctx.Request().Type { case rcon.SERVERDATA_AUTH: if s.Settings.AuthResponseDelay != 0 { time.Sleep(s.Settings.AuthResponseDelay) } s.authHandler(ctx) case rcon.SERVERDATA_EXECCOMMAND: if s.Settings.CommandResponseDelay != 0 { time.Sleep(s.Settings.CommandResponseDelay) } s.commandHandler(ctx) } } } // isRunning returns true if Server is running and false if is not. func (s *Server) isRunning() bool { select { case <-s.quit: return false default: return true } } // closeConn closes a client conn and removes it from connections map. func (s *Server) closeConn(conn net.Conn) { s.mu.Lock() defer s.mu.Unlock() if err := conn.Close(); err != nil { panic(fmt.Errorf("close conn error: %w", err)) } delete(s.connections, conn) }