Created
October 22, 2013 02:01
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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