Skip to content

Instantly share code, notes, and snippets.

@samuelmaddock
Created October 22, 2013 02:01
Show Gist options
  • Save samuelmaddock/7094095 to your computer and use it in GitHub Desktop.
Save samuelmaddock/7094095 to your computer and use it in GitHub Desktop.
Go script to query Valve's Master Server using the Master Server Query Protocol described here: https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
package main
import (
"net"
"os"
"bytes"
"encoding/binary"
"fmt"
"time"
)
const (
DefaultTimeout time.Duration = 8e9
MaxReplyLength = 1500
)
// https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol
// https://developer.valvesoftware.com/wiki/Server_queries
// http://stackoverflow.com/questions/12854125/go-how-do-i-dump-the-struct-into-the-byte-array-without-reflection
type MasterServerQuery struct {
msgtype, region byte
address, filter string
}
func NewQuery(msgtype, region byte, address, filter string) (q *MasterServerQuery) {
q = new(MasterServerQuery)
q.msgtype = msgtype
q.region = region
q.address = address
q.filter = filter
return
}
func (q *MasterServerQuery) encode() ([]byte, error) {
buf := new(bytes.Buffer)
// Message type
err := buf.WriteByte(q.msgtype)
if err != nil { return nil, err }
// Region code
err = buf.WriteByte(q.region)
if err != nil { return nil, err }
// Address
_, err = buf.WriteString(q.address)
if err != nil { return nil, err }
// Terminate address
err = buf.WriteByte(0x00)
if err != nil { return nil, err }
// Filter
_, err = buf.WriteString(q.filter)
if err != nil { return nil, err }
// Terminate filter
err = buf.WriteByte(0x00)
if err != nil { return nil, err }
return buf.Bytes(), nil
}
type SourceServerAddr struct {
ip [4]byte
port uint16
}
func (s SourceServerAddr) String() string {
return itod(uint(s.ip[0])) + "." +
itod(uint(s.ip[1])) + "." +
itod(uint(s.ip[2])) + "." +
itod(uint(s.ip[3])) + ":" +
itod(uint(s.port))
}
func main() {
// TODO: Attempt multiple Master Server IP addresses in the case of timeout
// serverAddr := "hl2master.steampowered.com:27015"
serverAddr := "208.64.200.52:27011"
// Resolve master server address
udpAddr, err := net.ResolveUDPAddr("udp4", serverAddr)
if err != nil {
fmt.Println("Failed to resolve UDP address of Master Server")
os.Exit(1)
}
fmt.Println("Address:", udpAddr)
// Connect to master server
conn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
fmt.Println("Failed to connect to master server")
os.Exit(1)
}
servers := getServers(conn)
// TODO: Query servers info using goroutines?
fmt.Printf("Total servers: %d\n", len(servers))
fmt.Println("Finished!")
os.Exit(0)
}
func getServers(conn *net.UDPConn) (servers []*SourceServerAddr) {
defer conn.Close()
// Initial query parameters
var msgtype byte = 0x31 // A2M_GET_SERVERS_BATCH2 packet
var region byte = 0xFF // Global region
lastServer := "0.0.0.0:0" // First query
// https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter
filter := "\\gamedir\\csgo" + // CS:GO servers
"\\map\\de_dust2" // 'de_dust2' map
// Create slice for storing server addresses
servers = make([]*SourceServerAddr, 0)
// Set connection timeout duration
conn.SetReadDeadline(time.Now().Add(DefaultTimeout))
// Query master server until we've received every server
for {
// Construct initial query packet
query := NewQuery(msgtype, region, lastServer, filter)
fmt.Println("Query:", query)
// Convert struct to array of bytes
qbuf, err := query.encode()
if err != nil {
fmt.Println("Failed to decode query")
break
}
// Send query
_, err = conn.Write(qbuf)
if err != nil {
fmt.Println("Failed to query master server")
break
}
fmt.Println("Sent query, awaiting reply...")
// Wait for reply
var reply [MaxReplyLength]byte
n, err := conn.Read(reply[0:])
if err != nil {
fmt.Println("Master server response timeout")
break
}
// Output response
fmt.Printf("Received response [%d]\n", n)
// Create buffer reader from reply
rbuf := bytes.NewBuffer(reply[0:n])
// Skip control and return characters
rbuf.Next(2)
var unique int32
err = binary.Read(rbuf, binary.LittleEndian, &unique)
if err != nil {
fmt.Println(err)
fmt.Println("Failed to read unique id from response data")
break
}
// Parse server addresses
var server SourceServerAddr
for i := 6; i < n; i = i + 6 {
err = binary.Read(rbuf, binary.LittleEndian, &server.ip)
if err != nil {
fmt.Println("Failed to read server address from buffer")
break
}
err = binary.Read(rbuf, binary.LittleEndian, &server.port)
if err != nil {
fmt.Println("Failed to read server address from buffer")
break
}
// Reverse bytes
server.port = (server.port & 0xff) << 8 | (server.port & 0xff00) >> 8
fmt.Println(server)
servers = append(servers, &server)
}
// Cache last server for next query
lastServer = server.String()
// End query if we've reached the last address
if lastServer == "0.0.0.0:0" {
servers = servers[:len(servers)-1] // pop last server
break
}
}
return
}
// from pkg/net/parse.go
// Convert i to decimal string.
func itod(i uint) string {
if i == 0 {
return "0"
}
// Assemble decimal in reverse order.
var b [32]byte
bp := len(b)
for ; i > 0; i /= 10 {
bp--
b[bp] = byte(i%10) + '0'
}
return string(b[bp:])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment