grcon/grcon.go

220 lines
5.6 KiB
Go
Raw Permalink Normal View History

/*
A Go written library for the RCON Protocol from Valve.
Information to the protocol can be found under:
https://developer.valvesoftware.com/wiki/Source_RCON_Protocol
*/
package grcon
import (
"bytes"
"encoding/binary"
"net"
"sync"
)
// NewRemoteConsole creates a new RemoteConsole with the given connection and default values.
func NewRemoteConsole(conn net.Conn) *RemoteConsole {
remoteConsole := &RemoteConsole{
Conn: conn,
// Initialize a buffer to hold at least a hole packet:
// Since the only one of the packet values that can change in length is the body,
// an easy way to calculate the size of a packet is to find the byte-length of the packet body,
// then add 10 to it.
ReadBuff: make([]byte, MaxPacket+sizeField),
}
return remoteConsole
}
// RemoteConsole holds the information to communicate withe remote console (server).
// To optain a preconfigured RemoteConsole use the NewRemoteConsole() function.
// The NewRemoteConsole() function is also the recommended way to get a *RemoteConsole.
//
// This struct can be used concurrently.
// All exported fields are not allowed to be nil!
type RemoteConsole struct {
// Conn is the connection to read and write to.
Conn net.Conn
// ReadBuff should at least have the capacity for a hole packet.
// Capacity >= MaxPacket + 4.
ReadBuff []byte
readMutex sync.Mutex
queuedBuff []byte
}
// Write writes a packet with a given id, type and body.
// The body should be a ASCII string.
// Returns an RequestTooLongError if the body is greater than the max
// length which is MaxPacket - MinPacket.
func (r *RemoteConsole) Write(packet Packet) error {
bodySize := size(len(packet.Body))
if bodySize > MaxBody {
return newRequestTooLongError()
}
buffer := bytes.NewBuffer(make([]byte, 0, MinPacket+sizeField+bodySize))
// size
err := binary.Write(buffer, binary.LittleEndian, bodySize+MinPacket)
if err != nil {
return err
}
// id
err = binary.Write(buffer, binary.LittleEndian, packet.Id)
if err != nil {
return err
}
// type
err = binary.Write(buffer, binary.LittleEndian, packet.Type)
if err != nil {
return err
}
// body
_, err = buffer.Write(packet.Body)
if err != nil {
return err
}
// double null termination
err = binary.Write(buffer, binary.LittleEndian, byte(0))
if err != nil {
return err
}
err = binary.Write(buffer, binary.LittleEndian, byte(0))
if err != nil {
return err
}
// writing to the connection
_, err = r.Conn.Write(buffer.Bytes())
return err
}
// Read returns all the parts of the read packet.
// Returns an ResponseTooLongError if the size of the packet is bigger
// than the MaxPacket siz. It can also return an UnexpectedForamatError
// if the packet size is smaller than the MinPacket size.
func (r *RemoteConsole) Read() (Packet, error) {
r.readMutex.Lock()
defer r.readMutex.Unlock()
var readBytes int
var err error
if r.queuedBuff != nil {
copy(r.ReadBuff, r.queuedBuff)
readBytes = len(r.queuedBuff)
r.queuedBuff = nil
} else {
readBytes, err = r.Conn.Read(r.ReadBuff)
if err != nil {
return Packet{}, err
}
}
dataSize, readBytes, err := r.readPacketSize(readBytes)
if err != nil {
return Packet{}, err
}
if dataSize > MaxPacket {
return Packet{}, newResponseTooLongError()
}
totalPacketSize := dataSize + sizeField
readBytes, err = r.readPacket(totalPacketSize, readBytes)
if err != nil {
return Packet{}, err
}
// The data has to be explicitly selected to prevent copying empty bytes.
data := r.ReadBuff[sizeField:totalPacketSize]
// Save not packet related bytes for the next read.
if readBytes > int(totalPacketSize) {
// start of the next buffer was at the end of this packet.
// save it for the next read.
// The data has to be explicitly selected to prevent copying empty bytes.
r.queuedBuff = r.ReadBuff[totalPacketSize:readBytes]
}
return r.parsePacket(data)
}
// readPacketSize wait until first 4 bytes are read to get the packet size.
// Takes as param how many bytes are already read. The returned size does not include the size field.
func (r *RemoteConsole) readPacketSize(readBytes int) (size, int, error) {
for readBytes < int(sizeField) {
// need the 4 byte packet size...
b, err := r.Conn.Read(r.ReadBuff[readBytes:])
if err != nil {
return 0, 0, err
}
readBytes += b
}
// Does not include the packetSize field.
var totalPacketSize size
b := bytes.NewBuffer(r.ReadBuff[:sizeField])
err := binary.Read(b, binary.LittleEndian, &totalPacketSize)
if err != nil {
return 0, 0, err
}
if totalPacketSize < MinPacket {
return 0, 0, newUnexpectedFormatError()
}
return totalPacketSize, readBytes, nil
}
// readPacket waits until the whole packet is read including the size field.
func (r *RemoteConsole) readPacket(totalPacketSize size, readBytes int) (int, error) {
for int(totalPacketSize) > readBytes {
b, err := r.Conn.Read(r.ReadBuff[readBytes:])
if err != nil {
return readBytes, err
}
readBytes += b
}
return readBytes, nil
}
// parsePacket reads the a packet from an array byte.
// The array has only to contain the 'id', 'type' and 'body' data.
func (r *RemoteConsole) parsePacket(data []byte) (Packet, error) {
var requestID, responseType int32
buffer := bytes.NewBuffer(data)
err := binary.Read(buffer, binary.LittleEndian, &requestID)
if err != nil {
return Packet{}, err
}
binary.Read(buffer, binary.LittleEndian, &responseType)
if err != nil {
return Packet{}, err
}
// the rest of the buffer is the body.
body := buffer.Bytes()
// remove the to null terminations
body = body[:len(body)-2]
parsedPacket := Packet{
Id: PacketId(requestID),
Type: PacketType(responseType),
Body: body,
}
return parsedPacket, nil
}