202 lines
4.4 KiB
Go
202 lines
4.4 KiB
Go
package a2s
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// Errors
|
|
var (
|
|
ErrPlayerRead = errors.New("failed to read player data")
|
|
ErrMultiPacketInvalid = errors.New("invalid multi-packet ID mismatch")
|
|
ErrMultiPacketMismatch = errors.New("multi-packet assembly failed")
|
|
errBzip2 = errors.New("bzip2 compressed response not supported")
|
|
)
|
|
|
|
// Flag type for request/response types
|
|
type Flag byte
|
|
|
|
const (
|
|
PlayerRequest Flag = 0x55
|
|
ChallengeResponse Flag = 0x41
|
|
|
|
DefaultBufferSize = 1400
|
|
DefaultDeadlineTimeout = 5
|
|
singlePacket uint32 = 0xFFFFFFFF
|
|
)
|
|
|
|
// Client handles connection and options
|
|
type Client struct {
|
|
Conn *net.UDPConn
|
|
Address *net.UDPAddr
|
|
Timeout time.Duration
|
|
BufferSize uint16
|
|
}
|
|
|
|
// New creates a new Client and dials the connection
|
|
func New(ip string, port int) (*Client, error) {
|
|
return NewWithAddr(&net.UDPAddr{IP: net.ParseIP(ip), Port: port})
|
|
}
|
|
|
|
func NewWithString(addr string) (*Client, error) {
|
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewWithAddr(udpAddr)
|
|
}
|
|
|
|
func NewWithAddr(addr *net.UDPAddr) (*Client, error) {
|
|
client := &Client{
|
|
Address: addr,
|
|
Timeout: DefaultDeadlineTimeout * time.Second,
|
|
BufferSize: DefaultBufferSize,
|
|
}
|
|
return client, client.Dial()
|
|
}
|
|
|
|
func (c *Client) Dial() error {
|
|
conn, err := net.DialUDP("udp", nil, c.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Conn = conn
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return c.Conn.Close()
|
|
}
|
|
|
|
func (c *Client) SetBufferSize(size uint16) {
|
|
c.BufferSize = size
|
|
}
|
|
|
|
func (c *Client) SetDeadlineTimeout(seconds int) {
|
|
c.Timeout = time.Duration(seconds) * time.Second
|
|
}
|
|
|
|
func (c *Client) Get(requestType Flag) ([]byte, Flag, time.Duration, error) {
|
|
resp, duration, err := c.request(requestType, singlePacket)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
flag := Flag(resp[4])
|
|
|
|
if flag == ChallengeResponse {
|
|
challenge := binary.BigEndian.Uint32(resp[5:9])
|
|
resp, _, err = c.request(requestType, challenge)
|
|
if err != nil {
|
|
return nil, 0, 0, err
|
|
}
|
|
flag = Flag(resp[4])
|
|
}
|
|
|
|
if err := validateResponseType(requestType, flag); err != nil {
|
|
return resp[5:], flag, duration, err
|
|
}
|
|
|
|
return resp[5:], flag, duration, nil
|
|
}
|
|
|
|
func (c *Client) request(requestType Flag, challenge uint32) ([]byte, time.Duration, error) {
|
|
req, err := createHeader(requestType, challenge)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
if _, err := c.Conn.Write(req); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := c.Conn.SetReadDeadline(time.Now().Add(c.Timeout)); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
resp := make([]byte, c.BufferSize)
|
|
n, err := c.Conn.Read(resp)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
|
|
multi, err := isMultiPacket(resp)
|
|
if err != nil {
|
|
return resp, 0, err
|
|
}
|
|
|
|
if !multi {
|
|
return resp[:n], duration, nil
|
|
}
|
|
|
|
packetID := binary.LittleEndian.Uint32(resp[4:8])
|
|
packetCount := int(resp[8] & 0x0F)
|
|
currentPacket := int(resp[9] & 0x0F)
|
|
|
|
if (packetID & 0x80000000) != 0 {
|
|
return nil, 0, errBzip2
|
|
}
|
|
|
|
packets := make(map[int][]byte)
|
|
packets[currentPacket] = resp[12:n]
|
|
|
|
for len(packets) < packetCount {
|
|
buf := make([]byte, c.BufferSize)
|
|
n, err := c.Conn.Read(buf)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if binary.LittleEndian.Uint32(buf[4:8]) != packetID {
|
|
return nil, 0, ErrMultiPacketInvalid
|
|
}
|
|
|
|
currentPacket = int(buf[9] & 0x0F)
|
|
if _, exists := packets[currentPacket]; !exists {
|
|
packets[currentPacket] = buf[12:n]
|
|
}
|
|
}
|
|
|
|
var assembledResp []byte
|
|
for i := 0; i < packetCount; i++ {
|
|
data, exists := packets[i]
|
|
if !exists {
|
|
return nil, 0, ErrMultiPacketMismatch
|
|
}
|
|
assembledResp = append(assembledResp, data...)
|
|
}
|
|
|
|
return assembledResp, duration, nil
|
|
}
|
|
|
|
// Helpers
|
|
|
|
func createHeader(requestType Flag, challenge uint32) ([]byte, error) {
|
|
header := []byte{0xFF, 0xFF, 0xFF, 0xFF, byte(requestType)}
|
|
if challenge != singlePacket {
|
|
challengeBytes := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(challengeBytes, challenge)
|
|
header = append(header, challengeBytes...)
|
|
}
|
|
return header, nil
|
|
}
|
|
|
|
func validateResponseType(request, response Flag) error {
|
|
if request == PlayerRequest && response != 0x44 {
|
|
return fmt.Errorf("unexpected player response flag: 0x%X", response)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isMultiPacket(buf []byte) (bool, error) {
|
|
if len(buf) < 4 {
|
|
return false, errors.New("packet too short")
|
|
}
|
|
header := binary.LittleEndian.Uint32(buf[:4])
|
|
return header == 0xFFFFFFFE, nil
|
|
}
|
|
|