Skip to content

Commit

Permalink
feat: add encryption to tf2e-v8 (#21)
Browse files Browse the repository at this point in the history
This adds encryption support to tf2e-v8

Added support for passing this key in through the cli and also by using a qstat style file input for bulk mode.
  • Loading branch information
lwaddicor authored Jan 24, 2022
1 parent 6cdc429 commit f282b4c
Show file tree
Hide file tree
Showing 14 changed files with 542 additions and 28 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
on: [push, pull_request]
name: Test
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.17
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: go test ./...
212 changes: 212 additions & 0 deletions cmd/cli/bulk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package main

import (
"bufio"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strings"

"github.com/multiplay/go-svrquery/lib/svrquery"
"github.com/multiplay/go-svrquery/lib/svrquery/protocol"
)

const (
numWorkers = 100

// maxQueries is the maximum number of queries that can be queried in one bulk request.
maxQueries = 10000
)

var (
errNoItem = errors.New("no item")
errEntryInvalid = errors.New("invalid entry")
)

// BulkResponseItem contains the information about the query being performed
// against a single server.
type BulkResponseItem struct {
Address string `json:"address"`
ServerInfo *BulkResponseServerInfoItem `json:"serverInfo,omitempty"`
Error string `json:"error,omitempty"`
}

// BulkResponseServerInfoItem containing basic server information.
type BulkResponseServerInfoItem struct {
CurrentPlayers int64 `json:"currentPlayers"`
MaxPlayers int64 `json:"maxPlayers"`
Map string `json:"map"`
}

// BulkResponseItemWork is an item returned by a worker containing the data item
// plus any terminal error it encountered.
type BulkResponseItemWork struct {
Item *BulkResponseItem
Err error
}

// queryBulk queries a bulk set of servers using a query file.
func queryBulk(file string) error {
// To simplify the workerpool load all the entries we are going to work on
lines := fileLines(file)

if len(lines) > maxQueries {
return fmt.Errorf("too many servers requested %d (max %d)", len(lines), maxQueries)
}

// Make a jobs channel and a number of workers to processes
// work off of the channel.
jobChan := make(chan string, len(lines))
resultsChan := make(chan BulkResponseItemWork)
for w := 1; w <= numWorkers; w++ {
go worker(jobChan, resultsChan)
}

items := make([]BulkResponseItem, 0, len(lines))

// Queue work onto the channel
for _, line := range lines {
jobChan <- line
}
close(jobChan)

// Receive results from workers.
var err error
for i := 0; i < len(lines); i++ {
v := <-resultsChan
switch {
case errors.Is(v.Err, errNoItem):
// Not fatal, but no response for this entry was created.
continue
case v.Err != nil:
// We had a major issue processing the list
if err == nil {
err = fmt.Errorf("fatal error: %w", v.Err)
continue
}
err = fmt.Errorf("additional error: %w", v.Err)
continue
}
// add the item to our list of items.
items = append(items, *v.Item)
}

if err != nil {
return err
}

b, err := json.MarshalIndent(items, "", "\t")
if err != nil {
return err
}
fmt.Printf("%s\n", b)
return nil
}

func fileLines(file string) []string {
f, err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer f.Close()

result := make([]string, 0, 1000)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
result = append(result, line)
}
return result
}

// worker is run in a goroutine to provide processing for the items.
func worker(jobChan <-chan string, results chan<- BulkResponseItemWork) {
for entry := range jobChan {
item, err := processBulkEntry(entry)
results <- BulkResponseItemWork{
Item: item,
Err: err,
}
}
}

// processBulkEntry processes an entry and returns an item containing the result or error.
func processBulkEntry(entry string) (*BulkResponseItem, error) {
querySection, addressSection, err := parseEntry(entry)
if err != nil {
return nil, fmt.Errorf("parse file entry: %w", err)
}

item := &BulkResponseItem{
Address: addressSection,
}

// If the query contains any options retrieve them and
querySection, options, err := parseOptions(querySection)
if err != nil {
// These errors are non fatal, as we know which server it is for
item.Error = err.Error()
return item, nil
}

if !protocol.Supported(querySection) {
item.Error = fmt.Sprintf("unsupported protocol: %s", querySection)
return item, nil
}

client, err := svrquery.NewClient(querySection, addressSection, options...)
if err != nil {
item.Error = fmt.Sprintf("create client: %s", err)
return item, nil
}

resp, err := client.Query()
if err != nil {
item.Error = fmt.Sprintf("query client: %s", err)
return item, nil
}

item.ServerInfo = &BulkResponseServerInfoItem{
CurrentPlayers: resp.NumClients(),
MaxPlayers: resp.MaxClients(),
Map: "UNKNOWN",
}

if currentMap, ok := resp.(protocol.Mapper); ok {
item.ServerInfo.Map = currentMap.Map()
}
return item, nil
}

func parseEntry(entry string) (querySection, addressSection string, err error) {
entry = strings.TrimSpace(entry)
if entry == "" {
return "", "", fmt.Errorf("process entry: %w", errNoItem)
}
sections := strings.Split(entry, " ")
if len(sections) != 2 {
return "", "", fmt.Errorf("%w: wrong number of sections", errEntryInvalid)
}

return sections[0], sections[1], nil
}

func parseOptions(querySection string) (baseQuery string, options []svrquery.Option, error error) {
options = make([]svrquery.Option, 0)
protocolSections := strings.Split(querySection, ",")
for i := 1; i < len(protocolSections); i++ {
keyVal := strings.SplitN(protocolSections[i], "=", 2)
if len(keyVal) != 2 {
return "", nil, fmt.Errorf("key value pair invalid: %v", keyVal)

}
// Only support key at the moment.
switch strings.ToLower(keyVal[0]) {
case "key":
options = append(options, svrquery.WithKey(keyVal[1]))
}
}
return protocolSections[0], options, nil
}
101 changes: 101 additions & 0 deletions cmd/cli/bulk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"testing"

"github.com/multiplay/go-svrquery/lib/svrquery"
"github.com/stretchr/testify/require"
)

func TestParseEntry(t *testing.T) {
testCases := []struct {
name string
input string
expQuery string
expAddress string
expErr error
}{
{
name: "ok",
input: "sqp 1.2.3.4:1234",
expQuery: "sqp",
expAddress: "1.2.3.4:1234",
},
{
name: "empty line",
input: "",
expErr: errNoItem,
},
{
name: "invalid entry",
input: "sqp 1.2.3.4:1234 extra",
expErr: errEntryInvalid,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
query, addr, err := parseEntry(tc.input)
if err != nil {
require.ErrorIs(t, err, tc.expErr)
return
}
require.NoError(t, err)
require.Equal(t, tc.expQuery, query)
require.Equal(t, tc.expAddress, addr)
})
}
}

func TestCreateClient(t *testing.T) {
testCases := []struct {
name string
query string
expQuery string
expKey string
expErr error
}{
{
name: "ok",
query: "tf2e",
expQuery: "tf2e",
},
{
name: "with_key",
query: "tf2e,key=val",
expKey: "val",
expQuery: "tf2e",
},
{
name: "with_unsupported_other",
query: "tf2e,other=val",
expQuery: "tf2e",
},
{
name: "invalid entry",
query: "tf2e",
expQuery: "tf2e",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
baseQuery, options, err := parseOptions(tc.query)
if err != nil {
require.ErrorIs(t, err, tc.expErr)
return
}
require.NoError(t, err)
require.Equal(t, tc.expQuery, baseQuery)

// Validate key setting
if tc.expKey != "" {
require.Len(t, options, 1)
c := svrquery.Client{}
require.NoError(t, options[0](&c))
require.Equal(t, tc.expKey, c.Key())
}
require.NotNil(t, options)
})
}
}
28 changes: 23 additions & 5 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@ import (
func main() {
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")
key := flag.String("key", "", "Key to use to authenticate")
file := flag.String("file", "", "Bulk file to execute to get basic server information")
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 *file != "" {
// Use bulk file mode
if err := queryBulk(*file); err != nil {
l.Fatal(err)
}
return
}

if *serverAddr != "" && *clientAddr != "" {
bail(l, "Cannot run both a server and a client. Specify either -addr OR -server flags")
}
Expand All @@ -36,20 +46,25 @@ func main() {
if *proto == "" {
bail(l, "Protocol required in server mode")
}
queryMode(l, *proto, *clientAddr)
queryMode(l, *proto, *clientAddr, *key)
default:
bail(l, "Please supply some options")
}
}

func queryMode(l *log.Logger, proto, address string) {
if err := query(proto, address); err != nil {
func queryMode(l *log.Logger, proto, address, key string) {
if err := query(proto, address, key); err != nil {
l.Fatal(err)
}
}

func query(proto, address string) error {
c, err := svrquery.NewClient(proto, address)
func query(proto, address, key string) error {
options := make([]svrquery.Option, 0)
if key != "" {
options = append(options, svrquery.WithKey(key))
}

c, err := svrquery.NewClient(proto, address, options...)
if err != nil {
return err
}
Expand Down Expand Up @@ -84,6 +99,9 @@ func server(l *log.Logger, proto, address string) error {
Map: "Map",
Port: 1000,
})
if err != nil {
return err
}

addr, err := net.ResolveUDPAddr("udp4", address)
if err != nil {
Expand Down
Loading

0 comments on commit f282b4c

Please sign in to comment.