Skip to content

Commit

Permalink
feat: Add sample SQP server (#15)
Browse files Browse the repository at this point in the history
This adds an extremely basic sample SQP server which can be queried.
  • Loading branch information
lwaddicor authored Jun 29, 2021
1 parent 12a23b0 commit cbc6351
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 16 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,40 @@ func main() {

CLI
-------------
A cli is available in https://github.com/multiplay/go-svrquery/tree/master/cmd/cli
A cli is available in github releases and also at https://github.com/multiplay/go-svrquery/tree/master/cmd/cli

This enables you make queries to servers using the specified protocol, and returns the response in pretty json.

### Client

```
./go-svrquery -addr localhost:12121 -proto sqp
{
"version": 1,
"address": "localhost:12121",
"server_info": {
"current_players": 1,
"max_players": 2,
"server_name": "Name",
"game_type": "Game Type",
"build_id": "",
"map": "Map",
"port": 1000
}
}
```

### Example Server

This tool also provides the ability to start a very basic sample server using a given protocol.

Currently, only `sqp` is supported

```
./go-svrquery -server :12121 -proto sqp
Starting sample server using protocol sqp on :12121
```

Documentation
-------------
- [GoDoc API Reference](http://godoc.org/github.com/multiplay/go-svrquery).
Expand Down
93 changes: 83 additions & 10 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,45 @@ import (
"flag"
"fmt"
"log"
"net"
"os"
"time"

"github.com/multiplay/go-svrquery/lib/svrquery"
"github.com/multiplay/go-svrquery/lib/svrsample"
"github.com/multiplay/go-svrquery/lib/svrsample/common"
)

func main() {
address := flag.String("addr", "", "Address e.g. 127.0.0.1:12345")
clientAddr := flag.String("addr", "", "Address to connect to e.g. 127.0.0.1:12345")
proto := flag.String("proto", "", "Protocol e.g. sqp, tf2e, tf2e-v7, tf2e-v8")
serverAddr := flag.String("server", "", "Address to start server e.g. 127.0.0.1:12121, :23232")
flag.Parse()

l := log.New(os.Stderr, "", 0)

if *address == "" {
l.Println("No address provided")
flag.PrintDefaults()
os.Exit(1)
if *serverAddr != "" && *clientAddr != "" {
bail(l, "Cannot run both a server and a client. Specify either -addr OR -server flags")
}

if *proto == "" {
l.Println("No protocol provided")
flag.PrintDefaults()
os.Exit(1)
switch {
case *serverAddr != "":
if *proto == "" {
bail(l, "No protocol provided in client mode")
}
serverMode(l, *proto, *serverAddr)
case *clientAddr != "":
if *proto == "" {
bail(l, "Protocol required in server mode")
}
queryMode(l, *proto, *clientAddr)
default:
bail(l, "Please supply some options")
}
}

if err := query(*proto, *address); err != nil {
func queryMode(l *log.Logger, proto, address string) {
if err := query(proto, address); err != nil {
l.Fatal(err)
}
}
Expand All @@ -53,3 +67,62 @@ func query(proto, address string) error {
fmt.Printf("%s\n", b)
return nil
}

func serverMode(l *log.Logger, proto, serverAddr string) {
if err := server(l, proto, serverAddr); err != nil {
l.Fatal(err)
}
}

func server(l *log.Logger, proto, address string) error {
l.Printf("Starting sample server using protocol %s on %s", proto, address)
responder, err := svrsample.GetResponder(proto, common.QueryState{
CurrentPlayers: 1,
MaxPlayers: 2,
ServerName: "Name",
GameType: "Game Type",
Map: "Map",
Port: 1000,
})

addr, err := net.ResolveUDPAddr("udp4", address)
if err != nil {
return err
}

conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return err
}

for {
buf := make([]byte, 16)
_, to, err := conn.ReadFromUDP(buf)
if err != nil {
l.Println("read from udp", err)
continue
}

resp, err := responder.Respond(to.String(), buf)
if err != nil {
l.Println("error responding to query", err)
continue
}

if err = conn.SetWriteDeadline(time.Now().Add(1 * time.Second)); err != nil {
l.Println("error setting write deadline")
continue
}

if _, err = conn.WriteTo(resp, to); err != nil {
l.Println("error writing response")
}
}

}

func bail(l *log.Logger, msg string) {
l.Println(msg)
flag.PrintDefaults()
os.Exit(1)
}
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -10,14 +9,12 @@ github.com/netdata/go-orchestrator v0.0.0-20190905093727-c793edba0e8f h1:jSzujNr
github.com/netdata/go-orchestrator v0.0.0-20190905093727-c793edba0e8f/go.mod h1:ECF8anFVCt/TfTIWVPgPrNaYJXtAtpAOF62ugDbw41A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
4 changes: 2 additions & 2 deletions lib/svrquery/protocol/titanfall/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestHealthFlags(t *testing.T) {
expPacketChokedOut bool
expSlowServerFrames bool
expHitching bool
expDOS bool
expDOS bool
}{
{
input: 0,
Expand Down Expand Up @@ -48,7 +48,7 @@ func TestHealthFlags(t *testing.T) {
expHitching: true,
},
{
input: 1 << 6,
input: 1 << 6,
expDOS: true,
},
}
Expand Down
8 changes: 8 additions & 0 deletions lib/svrsample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# SvrSample package

This server sample package provides sample libraries for providing basic responses to server query requests.
It is not designed to implement the full specs of protocols, however it will provide enough to respond to basic
server information requests.

The sample implementation here will be enough to satisfy the requirements for Multiplay's scaling system to query
the server for health, player counts and other useful information.
17 changes: 17 additions & 0 deletions lib/svrsample/common/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package common

// QueryResponder represents an interface to a concrete type which responds
// to query requests.
type QueryResponder interface {
Respond(clientAddress string, buf []byte) ([]byte, error)
}

// QueryState represents the state of a currently running game.
type QueryState struct {
CurrentPlayers int32
MaxPlayers int32
ServerName string
GameType string
Map string
Port uint16
}
67 changes: 67 additions & 0 deletions lib/svrsample/common/wire_encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package common

import (
"bytes"
"encoding/binary"
"reflect"
)

// WireEncoder is an interface which allows for different query implementations
// to write data to a byte buffer in a specific format.
type WireEncoder interface {
WriteString(resp *bytes.Buffer, s string) error
Write(resp *bytes.Buffer, v interface{}) error
}

// Encoder is a struct which implements proto.WireEncoder
type Encoder struct{}

// WriteString writes a string to the provided buffer.
func (e *Encoder) WriteString(resp *bytes.Buffer, s string) error {
if err := binary.Write(resp, binary.BigEndian, byte(len(s))); err != nil {
return err
}

return binary.Write(resp, binary.BigEndian, []byte(s))
}

// Write writes arbitrary data to the provided buffer.
func (e *Encoder) Write(resp *bytes.Buffer, v interface{}) error {
return binary.Write(resp, binary.BigEndian, v)
}

// WireWrite writes the provided data to resp with the provided WireEncoder w.
func WireWrite(resp *bytes.Buffer, w WireEncoder, data interface{}) error {
t := reflect.TypeOf(data)
vs := reflect.Indirect(reflect.ValueOf(data))
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
v := vs.FieldByName(f.Name)

// Dereference pointer
if f.Type.Kind() == reflect.Ptr {
if v.IsNil() {
continue
}
v = v.Elem()
}

switch v.Kind() {
case reflect.Struct:
if err := WireWrite(resp, w, v.Interface()); err != nil {
return err
}

case reflect.String:
if err := w.WriteString(resp, v.String()); err != nil {
return err
}

default:
if err := w.Write(resp, v.Interface()); err != nil {
return err
}
}
}
return nil
}
41 changes: 41 additions & 0 deletions lib/svrsample/protocol/sqp/server_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package sqp

import (
"github.com/multiplay/go-svrquery/lib/svrsample/common"
)

// ServerInfo holds the ServerInfo chunk data.
type ServerInfo struct {
CurrentPlayers uint16
MaxPlayers uint16
ServerName string
GameType string
BuildID string
GameMap string
Port uint16
}

// QueryStateToServerInfo converts proto.QueryState to ServerInfo.
func QueryStateToServerInfo(qs common.QueryState) ServerInfo {
return ServerInfo{
CurrentPlayers: uint16(qs.CurrentPlayers),
MaxPlayers: uint16(qs.MaxPlayers),
ServerName: qs.ServerName,
GameType: qs.GameType,
GameMap: qs.Map,
Port: qs.Port,
}
}

// Size returns the number of bytes sqpServerInfo will use on the wire.
func (si ServerInfo) Size() uint32 {
return uint32(
2 + // CurrentPlayers
2 + // MaxPlayers
len([]byte(si.ServerName)) + 1 +
len([]byte(si.GameType)) + 1 +
len([]byte(si.BuildID)) + 1 +
len([]byte(si.GameMap)) + 1 +
2, // Port
)
}
Loading

0 comments on commit cbc6351

Please sign in to comment.