Files
gamestatus/source/source.go

190 lines
4.1 KiB
Go

package source
import (
"encoding/json"
"net"
"strconv"
"strings"
"time"
a2s "github.com/rumblefrog/go-a2s"
"github.com/VexoraDevelopment/gamestatus/info"
)
type Options struct {
Timeout time.Duration
}
type Client struct {
opt Options
}
func New(opt Options) *Client {
if opt.Timeout <= 0 {
opt.Timeout = 2 * time.Second
}
return &Client{opt: opt}
}
func (c *Client) Connect(address string, port int) (*info.ServerInfo, error) {
return c.query(address, port, false)
}
func (c *Client) QueryFull(address string, port int) (*info.ServerInfo, error) {
return c.query(address, port, true)
}
func (c *Client) ConnectRust(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, false)
if err != nil {
return nil, err
}
out.Edition = info.EditionRust
return out, nil
}
func (c *Client) QueryFullRust(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, true)
if err != nil {
return nil, err
}
out.Edition = info.EditionRust
return out, nil
}
func (c *Client) ConnectCS2(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, false)
if err != nil {
return nil, err
}
out.Edition = info.EditionCS2
return out, nil
}
func (c *Client) QueryFullCS2(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, true)
if err != nil {
return nil, err
}
out.Edition = info.EditionCS2
return out, nil
}
func (c *Client) ConnectValve(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, false)
if err != nil {
return nil, err
}
out.Edition = info.EditionValve
return out, nil
}
func (c *Client) QueryFullValve(address string, port int) (*info.ServerInfo, error) {
out, err := c.query(address, port, true)
if err != nil {
return nil, err
}
out.Edition = info.EditionValve
return out, nil
}
func (c *Client) query(address string, port int, full bool) (*info.ServerInfo, error) {
addr := normalizeAddress(address, port)
client, err := a2s.NewClient(addr, a2s.TimeoutOption(c.opt.Timeout))
if err != nil {
return nil, err
}
defer client.Close()
start := time.Now()
infoResp, err := client.QueryInfo()
if err != nil {
return nil, err
}
out := &info.ServerInfo{
Edition: info.EditionSource,
Address: address,
IP: resolveIP(address),
Port: effectivePort(address, port),
MOTD: infoResp.Name,
Version: infoResp.Version,
Online: int(infoResp.Players),
Slots: int(infoResp.MaxPlayers),
Software: infoResp.Game,
Game: infoResp.Folder,
Map: infoResp.Map,
Query: full,
Latency: time.Since(start),
}
if full {
playerResp, err := client.QueryPlayer()
if err == nil && playerResp != nil {
names := make([]string, 0, len(playerResp.Players))
for _, p := range playerResp.Players {
if p == nil {
continue
}
name := strings.TrimSpace(p.Name)
if name != "" {
names = append(names, name)
}
}
out.Players = names
}
rulesResp, err := client.QueryRules()
if err == nil && rulesResp != nil {
out.RawQuery = rulesResp.Rules
}
}
raw, _ := json.Marshal(infoResp)
out.Raw = raw
return out, nil
}
func normalizeAddress(host string, port int) string {
if port == 0 {
port = a2s.DefaultPort
}
if strings.Contains(host, ":") {
if _, _, err := net.SplitHostPort(host); err == nil {
return host
}
if strings.Count(host, ":") > 1 && !strings.HasPrefix(host, "[") {
return net.JoinHostPort(host, strconv.Itoa(port))
}
}
return net.JoinHostPort(host, strconv.Itoa(port))
}
func effectivePort(host string, port int) int {
if port != 0 {
return port
}
if _, p, err := net.SplitHostPort(host); err == nil {
if v, convErr := strconv.Atoi(p); convErr == nil {
return v
}
}
return a2s.DefaultPort
}
func resolveIP(host string) string {
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
host = strings.TrimPrefix(host, "[")
host = strings.TrimSuffix(host, "]")
if ip := net.ParseIP(host); ip != nil {
return ip.String()
}
ips, err := net.LookupIP(host)
if err != nil || len(ips) == 0 {
return ""
}
return ips[0].String()
}