Initial TeamSpeak 3 ServerQuery client
Initial version of the TeamSpeak 3 ServerQuery client with function command support for: Basic: Login, Logout, Version, Use, UsePort Server: List, IDGetByPort, Info, Create, Edit, Delete, Start, Stop, GroupList, PrivilegeKeyList, PrivilegeKeyAdd All commands are support via flexible command and argument processing, including automatic decoding of returned data.
This commit is contained in:
Родитель
0be7b57c66
Коммит
8881492360
|
@ -0,0 +1,2 @@
|
|||
*.swp
|
||||
*.tmp
|
|
@ -0,0 +1,16 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.8.x
|
||||
- tip
|
||||
- master
|
||||
|
||||
install:
|
||||
- go get github.com/stretchr/testify/assert
|
||||
- go get github.com/mitchellh/mapstructure
|
||||
- go get -u gopkg.in/alecthomas/gometalinter.v1
|
||||
- gometalinter.v1 --install
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
- gometalinter.v1 --cyclo-over=15 ./...
|
56
README.md
56
README.md
|
@ -1,2 +1,54 @@
|
|||
# go-teamspeak3
|
||||
A golang TeamSpeak 3 ServerQuery client
|
||||
# TeamSpeak 3 [![Go Report Card](https://goreportcard.com/badge/github.com/multiplay/go-ts3)](https://goreportcard.com/report/github.com/multiplay/go-ts3) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](https://github.com/multiplay/go-ts3/blob/master/LICENSE) [![GoDoc](https://godoc.org/github.com/multiplay/go-ts3?status.svg)](https://godoc.org/github.com/multiplay/go-ts3) [![Build Status](https://travis-ci.org/multiplay/go-ts3.svg?branch=master)](https://travis-ci.org/multiplay/go-ts3)
|
||||
|
||||
go-ts3 is a [Go](http://golang.org/) client for the [TeamSpeak 3 ServerQuery Protocol](http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf).
|
||||
|
||||
Features
|
||||
--------
|
||||
* [ServerQuery](http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf) Support.
|
||||
|
||||
Installation
|
||||
------------
|
||||
```sh
|
||||
go get -u github.com/multiplay/go-ts3
|
||||
```
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Using go-ts3 is simple just create a client, login and then send commands e.g.
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/multiplay/go-ts3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c, err := ts3.NewClient("192.168.1.102:10011")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err := c.Login(user, pass); err != nil {
|
||||
log.Fatal(er)
|
||||
}
|
||||
|
||||
if v, err := c.Version(); err != nil {
|
||||
log.Fatal(er)
|
||||
} else {
|
||||
log.Println("server is running:", v)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
- [GoDoc API Reference](http://godoc.org/github.com/multiplay/go-ts3).
|
||||
|
||||
License
|
||||
-------
|
||||
go-ts3 is available under the [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause).
|
||||
```
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package ts3
|
||||
|
||||
// Login authenticates with the server.
|
||||
func (c *Client) Login(user, passwd string) error {
|
||||
_, err := c.ExecCmd(NewCmd("login").WithArgs(
|
||||
NewArg("client_login_name", user),
|
||||
NewArg("client_login_password", passwd)),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logout deselect virtual server and log out.
|
||||
func (c *Client) Logout() error {
|
||||
_, err := c.Exec("logout")
|
||||
return err
|
||||
}
|
||||
|
||||
// Version represents version information.
|
||||
type Version struct {
|
||||
Version string
|
||||
Platform string
|
||||
Build int
|
||||
}
|
||||
|
||||
// Version returns version information.
|
||||
func (c *Client) Version() (*Version, error) {
|
||||
v := &Version{}
|
||||
if _, err := c.ExecCmd(NewCmd("version").WithResponse(v)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Use selects a virtual server by id.
|
||||
func (c *Client) Use(id int) error {
|
||||
_, err := c.ExecCmd(NewCmd("use").WithArgs(NewArg("sid", id)))
|
||||
return err
|
||||
}
|
||||
|
||||
// UsePort selects a virtual server by port.
|
||||
func (c *Client) UsePort(port int) error {
|
||||
_, err := c.ExecCmd(NewCmd("use").WithArgs(NewArg("port", port)))
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCmdsBasic(t *testing.T) {
|
||||
s := newServer(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second*2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
assert.NoError(t, c.Close())
|
||||
}()
|
||||
|
||||
auth := func(t *testing.T) {
|
||||
if err = c.Login("user", "pass"); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.Logout(); !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
version := func(t *testing.T) {
|
||||
v, err := c.Version()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "3.0.12.2", v.Version)
|
||||
assert.Equal(t, 1455547898, v.Build)
|
||||
assert.Equal(t, "FreeBSD", v.Platform)
|
||||
}
|
||||
|
||||
useID := func(t *testing.T) {
|
||||
assert.NoError(t, c.Use(1))
|
||||
}
|
||||
|
||||
usePort := func(t *testing.T) {
|
||||
assert.NoError(t, c.UsePort(1024))
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
f func(t *testing.T)
|
||||
}{
|
||||
{"auth", auth},
|
||||
{"version", version},
|
||||
{"useid", useID},
|
||||
{"userport", usePort},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, tc.f)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultPort is the default TeamSpeak 3 ServerQuery port.
|
||||
DefaultPort = 10011
|
||||
|
||||
connectHeader = "TS3"
|
||||
)
|
||||
|
||||
var (
|
||||
respTrailerRe = regexp.MustCompile(`^error id=(\d+) msg=([^ ]+)(.*)`)
|
||||
|
||||
// DefaultTimeout is the default read / write / dial timeout for Clients.
|
||||
DefaultTimeout = time.Second * 10
|
||||
)
|
||||
|
||||
// Client is a TeamSpeak 3 ServerQuery client.
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
timeout time.Duration
|
||||
scanner *bufio.Scanner
|
||||
|
||||
Server *ServerMethods
|
||||
}
|
||||
|
||||
// Timeout sets read / write / dial timeout for a TeamSpeak 3 Client.
|
||||
func Timeout(timeout time.Duration) func(*Client) error {
|
||||
return func(c *Client) error {
|
||||
c.timeout = timeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient returns a new TeamSpeak 3 client connected to addr.
|
||||
func NewClient(addr string, options ...func(c *Client) error) (*Client, error) {
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = fmt.Sprintf("%v:%v", addr, DefaultPort)
|
||||
}
|
||||
|
||||
c := &Client{timeout: DefaultTimeout}
|
||||
for _, f := range options {
|
||||
if f == nil {
|
||||
return nil, ErrNilOption
|
||||
}
|
||||
if err := f(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up command groups
|
||||
c.Server = &ServerMethods{Client: c}
|
||||
|
||||
var err error
|
||||
if c.conn, err = net.DialTimeout("tcp", addr, c.timeout); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.scanner = bufio.NewScanner(bufio.NewReader(c.conn))
|
||||
c.scanner.Split(ScanLines)
|
||||
|
||||
if err := c.setDeadline(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reader the connection header
|
||||
if !c.scanner.Scan() {
|
||||
return nil, c.scanErr()
|
||||
}
|
||||
|
||||
if l := c.scanner.Text(); l != connectHeader {
|
||||
return nil, fmt.Errorf("invalid connection header %q", l)
|
||||
}
|
||||
|
||||
// Slurp the banner
|
||||
if !c.scanner.Scan() {
|
||||
return nil, c.scanErr()
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// setDeadline updates the deadline on the connection based on the clients configured timeout.
|
||||
func (c *Client) setDeadline() error {
|
||||
return c.conn.SetDeadline(time.Now().Add(c.timeout))
|
||||
}
|
||||
|
||||
// Exec executes cmd on the server and returns the response.
|
||||
func (c *Client) Exec(cmd string) ([]string, error) {
|
||||
return c.ExecCmd(NewCmd(cmd))
|
||||
}
|
||||
|
||||
// ExecCmd executes cmd on the server and returns the response.
|
||||
func (c *Client) ExecCmd(cmd *Cmd) ([]string, error) {
|
||||
if err := c.setDeadline(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := c.conn.Write([]byte(cmd.String())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.setDeadline(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := make([]string, 0, 10)
|
||||
for c.scanner.Scan() {
|
||||
l := c.scanner.Text()
|
||||
if l == "error id=0 msg=ok" {
|
||||
if cmd.response != nil {
|
||||
if err := DecodeResponse(lines, cmd.response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return lines, nil
|
||||
} else if matches := respTrailerRe.FindStringSubmatch(l); len(matches) == 4 {
|
||||
return nil, NewError(matches)
|
||||
} else {
|
||||
lines = append(lines, l)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, c.scanErr()
|
||||
}
|
||||
|
||||
// Close closes the connection to the server.
|
||||
func (c *Client) Close() error {
|
||||
_, err := c.Exec("quit")
|
||||
err2 := c.conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err2
|
||||
}
|
||||
|
||||
// scanError returns the error from the scanner if non-nil,
|
||||
// io.ErrUnexpectedEOF otherwise.
|
||||
func (c *Client) scanErr() error {
|
||||
if err := c.scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
s := newServer(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second*2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
assert.Error(t, c.Close())
|
||||
}()
|
||||
|
||||
_, err = c.Exec("version")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = c.ExecCmd(NewCmd("version"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = c.ExecCmd(NewCmd("invalid"))
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = c.ExecCmd(NewCmd("disconnect"))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClientNilOption(t *testing.T) {
|
||||
_, err := NewClient("", nil)
|
||||
if !assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, ErrNilOption, err)
|
||||
}
|
||||
|
||||
func TestClientOptionError(t *testing.T) {
|
||||
errBadOption := errors.New("bad option")
|
||||
_, err := NewClient("", func(c *Client) error { return errBadOption })
|
||||
if !assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, errBadOption, err)
|
||||
}
|
||||
|
||||
func TestClientDisconnect(t *testing.T) {
|
||||
s := newServer(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second*2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, c.Close())
|
||||
|
||||
_, err = c.Exec("version")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClientWriteFail(t *testing.T) {
|
||||
s := newServer(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second*2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.NoError(t, c.conn.(*net.TCPConn).CloseWrite())
|
||||
|
||||
_, err = c.Exec("version")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClientDialFail(t *testing.T) {
|
||||
c, err := NewClient("127.0.0.1", Timeout(time.Nanosecond))
|
||||
if assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
assert.NoError(t, c.Close())
|
||||
}
|
||||
|
||||
func TestClientNoHeader(t *testing.T) {
|
||||
s := newServerStopped(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.noHeader = true
|
||||
s.Start()
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second))
|
||||
if assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
assert.NoError(t, c.Close())
|
||||
}
|
||||
|
||||
func TestClientNoBanner(t *testing.T) {
|
||||
s := newServerStopped(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.noBanner = true
|
||||
s.Start()
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second))
|
||||
if assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
assert.NoError(t, c.Close())
|
||||
}
|
||||
|
||||
func TestClientFailConn(t *testing.T) {
|
||||
s := newServerStopped(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.failConn = true
|
||||
s.Start()
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second))
|
||||
if assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
assert.NoError(t, c.Close())
|
||||
}
|
||||
|
||||
func TestClientBadHeader(t *testing.T) {
|
||||
s := newServerStopped(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.badHeader = true
|
||||
s.Start()
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second))
|
||||
if assert.Error(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
// Should never get here
|
||||
assert.NoError(t, c.Close())
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cmd represents a TeamSpeak 3 ServerQuery command.
|
||||
type Cmd struct {
|
||||
cmd string
|
||||
args []CmdArg
|
||||
options []string
|
||||
response interface{}
|
||||
}
|
||||
|
||||
// NewCmd creates a new Cmd.
|
||||
func NewCmd(cmd string) *Cmd {
|
||||
return &Cmd{cmd: cmd}
|
||||
}
|
||||
|
||||
// WithArgs sets the command Args.
|
||||
func (c *Cmd) WithArgs(args ...CmdArg) *Cmd {
|
||||
c.args = args
|
||||
return c
|
||||
}
|
||||
|
||||
// WithOptions sets the command Options.
|
||||
func (c *Cmd) WithOptions(options ...string) *Cmd {
|
||||
c.options = options
|
||||
return c
|
||||
}
|
||||
|
||||
// WithResponse sets the command Response which will have the data returned from the server decoded into it.
|
||||
func (c *Cmd) WithResponse(r interface{}) *Cmd {
|
||||
c.response = r
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cmd) String() string {
|
||||
args := make([]interface{}, 1, len(c.args)+len(c.options)+1)
|
||||
args[0] = c.cmd
|
||||
for _, v := range c.args {
|
||||
args = append(args, v.ArgString())
|
||||
}
|
||||
for _, v := range c.options {
|
||||
args = append(args, v)
|
||||
}
|
||||
return fmt.Sprintln(args...)
|
||||
}
|
||||
|
||||
// CmdArg is implemented by types which can be used as a command argument.
|
||||
type CmdArg interface {
|
||||
ArgString() string
|
||||
}
|
||||
|
||||
// ArgGroup represents a group of TeamSpeak 3 ServerQuery command arguments.
|
||||
type ArgGroup struct {
|
||||
grp []CmdArg
|
||||
}
|
||||
|
||||
// NewArgGroup returns a new ArgGroup.
|
||||
func NewArgGroup(args ...CmdArg) *ArgGroup {
|
||||
return &ArgGroup{grp: args}
|
||||
}
|
||||
|
||||
// ArgString implements CmdArg.
|
||||
func (ag *ArgGroup) ArgString() string {
|
||||
args := make([]string, len(ag.grp))
|
||||
for i, arg := range ag.grp {
|
||||
args[i] = arg.ArgString()
|
||||
}
|
||||
return strings.Join(args, "|")
|
||||
}
|
||||
|
||||
// ArgSet represents a set of TeamSpeak 3 ServerQuery command arguments.
|
||||
type ArgSet struct {
|
||||
set []CmdArg
|
||||
}
|
||||
|
||||
// NewArgSet returns a new ArgSet.
|
||||
func NewArgSet(args ...CmdArg) *ArgSet {
|
||||
return &ArgSet{set: args}
|
||||
}
|
||||
|
||||
// ArgString implements CmdArg.
|
||||
func (ag *ArgSet) ArgString() string {
|
||||
args := make([]string, len(ag.set))
|
||||
for i, arg := range ag.set {
|
||||
args[i] = arg.ArgString()
|
||||
}
|
||||
return strings.Join(args, " ")
|
||||
}
|
||||
|
||||
// Arg represents a TeamSpeak 3 ServerQuery command argument.
|
||||
// Args automatically escape white space and special characters before being sent to the server.
|
||||
type Arg struct {
|
||||
key string
|
||||
val string
|
||||
}
|
||||
|
||||
// NewArg returns a new Arg with key val.
|
||||
func NewArg(key string, val interface{}) *Arg {
|
||||
return &Arg{key: key, val: fmt.Sprint(val)}
|
||||
}
|
||||
|
||||
// ArgString implements CmdArg
|
||||
func (a *Arg) ArgString() string {
|
||||
return fmt.Sprintf("%v=%v", encoder.Replace(a.key), encoder.Replace(a.val))
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cmd *Cmd
|
||||
expect string
|
||||
}{
|
||||
{"cmd", NewCmd("version"),
|
||||
"version",
|
||||
},
|
||||
{"arg", NewCmd("use").
|
||||
WithArgs(NewArg("sid", 1)),
|
||||
"use sid=1",
|
||||
},
|
||||
{"args", NewCmd("use").
|
||||
WithArgs(NewArg("sid", 1), NewArg("port", 1234)),
|
||||
"use sid=1 port=1234",
|
||||
},
|
||||
{"args-option", NewCmd("use").
|
||||
WithArgs(NewArg("sid", 1), NewArg("port", 1234)).
|
||||
WithOptions("-virtual"),
|
||||
"use sid=1 port=1234 -virtual",
|
||||
},
|
||||
{"options", NewCmd("serverlist").
|
||||
WithOptions("-uid", "-short"),
|
||||
"serverlist -uid -short",
|
||||
},
|
||||
{"arg-group-single", NewCmd("servergroupdelperm").
|
||||
WithArgs(
|
||||
NewArg("sgid", 1),
|
||||
NewArgGroup(NewArg("permid", 1), NewArg("permid", 2)),
|
||||
),
|
||||
"servergroupdelperm sgid=1 permid=1|permid=2",
|
||||
},
|
||||
{"arg-group-multi", NewCmd("servergroupaddperm").
|
||||
WithArgs(
|
||||
NewArg("sgid", 1),
|
||||
NewArgGroup(
|
||||
NewArgSet(
|
||||
NewArg("permid", 1),
|
||||
NewArg("permvalue", 1),
|
||||
NewArg("permnegated", 0),
|
||||
NewArg("permskip", 0),
|
||||
),
|
||||
NewArgSet(
|
||||
NewArg("permid", 2),
|
||||
NewArg("permvalue", 2),
|
||||
NewArg("permnegated", 1),
|
||||
NewArg("permskip", 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
"servergroupaddperm sgid=1 permid=1 permvalue=1 permnegated=0 permskip=0|permid=2 permvalue=2 permnegated=1 permskip=1",
|
||||
},
|
||||
{"escaped-chars", NewCmd("servergroupadd").
|
||||
WithArgs(NewArg("name", "Chars:\\/ |\a\b\f\n\r\t\v")),
|
||||
`servergroupadd name=Chars:\\\/\s\p\a\b\f\n\r\t\v`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expect+"\n", tc.cmd.String())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidConnectHeader is returned by NewClient if the server doesn't respond with the required connection header.
|
||||
ErrInvalidConnectHeader = errors.New("invalid connect header")
|
||||
|
||||
// ErrNilOption is returned by NewClient if an option is nil.
|
||||
ErrNilOption = errors.New("nil option")
|
||||
)
|
||||
|
||||
// Error represents a error returned from the TeamSpeak 3 server.
|
||||
type Error struct {
|
||||
ID int
|
||||
Msg string
|
||||
Details map[string]interface{}
|
||||
}
|
||||
|
||||
// NewError returns a new Error parsed from TeamSpeak 3 server response.
|
||||
func NewError(matches []string) *Error {
|
||||
e := &Error{Msg: Decode(matches[2])}
|
||||
|
||||
var err error
|
||||
if e.ID, err = strconv.Atoi(matches[1]); err != nil {
|
||||
// This should be impossible given it matched \d+ in the regexp.
|
||||
e.ID = -1
|
||||
}
|
||||
|
||||
if rem := strings.TrimSpace(matches[3]); rem != "" {
|
||||
e.Details = make(map[string]interface{})
|
||||
for _, s := range strings.Split(rem, " ") {
|
||||
d := strings.SplitN(s, "=", 2)
|
||||
v := Decode(d[0])
|
||||
if i, err := strconv.Atoi(d[1]); err == nil {
|
||||
e.Details[v] = i
|
||||
} else {
|
||||
e.Details[v] = Decode(d[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if len(e.Details) > 0 {
|
||||
return fmt.Sprintf("%v %v (%v)", e.Msg, e.Details, e.ID)
|
||||
}
|
||||
return fmt.Sprintf("%v (%v)", e.Msg, e.ID)
|
||||
}
|
||||
|
||||
// InvalidResponseError is the error returned when the response data was invalid.
|
||||
type InvalidResponseError struct {
|
||||
Reason string
|
||||
Data []string
|
||||
}
|
||||
|
||||
// NewInvalidResponseError returns a new InvalidResponseError from lines.
|
||||
func NewInvalidResponseError(reason string, lines []string) *InvalidResponseError {
|
||||
return &InvalidResponseError{Reason: reason, Data: lines}
|
||||
}
|
||||
|
||||
func (e *InvalidResponseError) Error() string {
|
||||
return fmt.Sprintf("%v (%+v)", e.Reason, e.Data)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *Error
|
||||
}{
|
||||
{"ok",
|
||||
`error id=0 msg=ok`,
|
||||
&Error{Msg: "ok"},
|
||||
},
|
||||
{"invalid-server",
|
||||
`error id=1024 msg=invalid\sserverID`,
|
||||
&Error{
|
||||
ID: 1024,
|
||||
Msg: "invalid serverID",
|
||||
},
|
||||
},
|
||||
{"permission",
|
||||
`error id=2568 msg=insufficient\sclient\spermissions failed_permid=4 other=test`,
|
||||
&Error{
|
||||
ID: 2568,
|
||||
Msg: "insufficient client permissions",
|
||||
Details: map[string]interface{}{"failed_permid": 4, "other": "test"},
|
||||
},
|
||||
},
|
||||
{"invalid",
|
||||
` error id=0 msg=ok`,
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
matches := respTrailerRe.FindStringSubmatch(tc.line)
|
||||
if tc.expected == nil {
|
||||
assert.Equal(t, 0, len(matches))
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(t, 4, len(matches)) {
|
||||
return
|
||||
}
|
||||
err := NewError(matches)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tc.expected, err)
|
||||
assert.NotEmpty(t, err.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewInvalidResponseError(t *testing.T) {
|
||||
reason := "my reason"
|
||||
lines := []string{"line1"}
|
||||
err := NewInvalidResponseError(reason, lines)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), err.Reason)
|
||||
assert.Contains(t, err.Error(), err.Data[0])
|
||||
assert.Equal(t, reason, err.Reason)
|
||||
assert.Equal(t, lines, err.Data)
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var (
|
||||
// encoder performs white space and special character encoding
|
||||
// as required by the ServerQuery protocol.
|
||||
encoder = strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`/`, `\/`,
|
||||
` `, `\s`,
|
||||
`|`, `\p`,
|
||||
"\a", `\a`,
|
||||
"\b", `\b`,
|
||||
"\f", `\f`,
|
||||
"\n", `\n`,
|
||||
"\r", `\r`,
|
||||
"\t", `\t`,
|
||||
"\v", `\v`,
|
||||
)
|
||||
|
||||
// decoder performs white space and special character decoding
|
||||
// as required by the ServerQuery protocol.
|
||||
decoder = strings.NewReplacer(
|
||||
`\\`, "\\",
|
||||
`\/`, "/",
|
||||
`\s`, " ",
|
||||
`\p`, "|",
|
||||
`\a`, "\a",
|
||||
`\b`, "\b",
|
||||
`\f`, "\f",
|
||||
`\n`, "\n",
|
||||
`\r`, "\r",
|
||||
`\t`, "\t",
|
||||
`\v`, "\v",
|
||||
)
|
||||
)
|
||||
|
||||
// Decode returns a decoded version of str.
|
||||
func Decode(str string) string {
|
||||
return decoder.Replace(str)
|
||||
}
|
||||
|
||||
// DecodeResponse decodes a response into a struct.
|
||||
func DecodeResponse(lines []string, v interface{}) error {
|
||||
if len(lines) != 1 {
|
||||
return NewInvalidResponseError("too many lines", lines)
|
||||
}
|
||||
|
||||
input := make(map[string]interface{})
|
||||
value := reflect.ValueOf(v)
|
||||
var slice reflect.Value
|
||||
var elemType reflect.Type
|
||||
if value.Kind() == reflect.Ptr {
|
||||
slice = value.Elem()
|
||||
if slice.Kind() == reflect.Slice {
|
||||
elemType = slice.Type().Elem()
|
||||
}
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(lines[0], "|") {
|
||||
for _, val := range strings.Split(part, " ") {
|
||||
parts := strings.SplitN(val, "=", 2)
|
||||
// TODO(steve): support groups
|
||||
key := Decode(parts[0])
|
||||
if len(parts) == 2 {
|
||||
v := Decode(parts[1])
|
||||
if i, err := strconv.Atoi(v); err != nil {
|
||||
input[key] = v
|
||||
} else {
|
||||
input[key] = i
|
||||
}
|
||||
} else {
|
||||
input[key] = ""
|
||||
}
|
||||
}
|
||||
|
||||
if elemType != nil {
|
||||
// Expecting a slice
|
||||
if err := decodeSlice(elemType, slice, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset the input map
|
||||
input = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if elemType != nil {
|
||||
// Expecting a slice, already decoded
|
||||
return nil
|
||||
}
|
||||
|
||||
return decodeMap(input, v)
|
||||
}
|
||||
|
||||
// decodeMap decodes input into r.
|
||||
func decodeMap(d map[string]interface{}, r interface{}) error {
|
||||
cfg := &mapstructure.DecoderConfig{
|
||||
WeaklyTypedInput: true,
|
||||
TagName: "ms",
|
||||
Result: r,
|
||||
}
|
||||
dec, err := mapstructure.NewDecoder(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dec.Decode(d)
|
||||
}
|
||||
|
||||
// decodeSlice decodes input into slice.
|
||||
func decodeSlice(elemType reflect.Type, slice reflect.Value, input map[string]interface{}) error {
|
||||
var v reflect.Value
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
v = reflect.New(elemType.Elem())
|
||||
} else {
|
||||
v = reflect.New(elemType)
|
||||
}
|
||||
|
||||
if !v.CanInterface() {
|
||||
return fmt.Errorf("can't interface %#v", v)
|
||||
}
|
||||
|
||||
if err := decodeMap(input, v.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if elemType.Kind() == reflect.Struct {
|
||||
v = v.Elem()
|
||||
}
|
||||
slice.Set(reflect.Append(slice, v))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
str := "\\/ |\a\b\f\n\r\t\v"
|
||||
assert.Equal(t, `\\\/\s\p\a\b\f\n\r\t\v`, encoder.Replace(str))
|
||||
assert.Equal(t, str, Decode(encoder.Replace(str)))
|
||||
}
|
||||
|
||||
type testResp struct {
|
||||
Response string
|
||||
ID int
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func TestDecodeResponse(t *testing.T) {
|
||||
r := &testResp{}
|
||||
expected := &testResp{
|
||||
Response: "test",
|
||||
ID: 1,
|
||||
Valid: false,
|
||||
}
|
||||
assert.NoError(t, DecodeResponse([]string{"response=test id=1 valid"}, r))
|
||||
assert.Equal(t, expected, r)
|
||||
|
||||
assert.Error(t, DecodeResponse([]string{"line1", "line2"}, r))
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
cmdQuit = "quit"
|
||||
banner = `Welcome to the TeamSpeak 3 ServerQuery interface, type "help" for a list of commands and "help <command>" for information on a specific command.`
|
||||
|
||||
errUnknownCmd = `error id=256 msg=command\snot\sfound`
|
||||
errOK = `error id=0 msg=ok`
|
||||
)
|
||||
|
||||
var (
|
||||
commands = map[string]string{
|
||||
"version": "version=3.0.12.2 build=1455547898 platform=FreeBSD",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"use": "",
|
||||
"serverlist": `virtualserver_id=1 virtualserver_port=10677 virtualserver_status=online virtualserver_clientsonline=1 virtualserver_queryclientsonline=1 virtualserver_maxclients=35 virtualserver_uptime=12345025 virtualserver_name=Server\s#1 virtualserver_autostart=1 virtualserver_machine_id=1 virtualserver_unique_identifier=uniq1|virtualserver_id=2 virtualserver_port=10617 virtualserver_status=online virtualserver_clientsonline=3 virtualserver_queryclientsonline=2 virtualserver_maxclients=10 virtualserver_uptime=3165117 virtualserver_name=Server\s#2 virtualserver_autostart=1 virtualserver_machine_id=1 virtualserver_unique_identifier=uniq2`,
|
||||
"serverinfo": `virtualserver_antiflood_points_needed_command_block=150 virtualserver_antiflood_points_needed_ip_block=250 virtualserver_antiflood_points_tick_reduce=5 virtualserver_channel_temp_delete_delay_default=0 virtualserver_codec_encryption_mode=0 virtualserver_complain_autoban_count=5 virtualserver_complain_autoban_time=1200 virtualserver_complain_remove_time=3600 virtualserver_created=0 virtualserver_default_channel_admin_group=1 virtualserver_default_channel_group=4 virtualserver_default_server_group=5 virtualserver_download_quota=18446744073709551615 virtualserver_filebase=files virtualserver_flag_password=0 virtualserver_hostbanner_gfx_interval=0 virtualserver_hostbanner_gfx_url virtualserver_hostbanner_mode=0 virtualserver_hostbanner_url virtualserver_hostbutton_gfx_url virtualserver_hostbutton_tooltip=Multiplay\sGame\sServers virtualserver_hostbutton_url=http:\/\/www.multiplaygameservers.com virtualserver_hostmessage virtualserver_hostmessage_mode=0 virtualserver_icon_id=0 virtualserver_log_channel=0 virtualserver_log_client=0 virtualserver_log_filetransfer=0 virtualserver_log_permissions=1 virtualserver_log_query=0 virtualserver_log_server=0 virtualserver_max_download_total_bandwidth=18446744073709551615 virtualserver_max_upload_total_bandwidth=18446744073709551615 virtualserver_maxclients=32 virtualserver_min_android_version=0 virtualserver_min_client_version=0 virtualserver_min_clients_in_channel_before_forced_silence=100 virtualserver_min_ios_version=0 virtualserver_name=Test\sServer virtualserver_name_phonetic virtualserver_needed_identity_security_level=8 virtualserver_password virtualserver_priority_speaker_dimm_modificator=-18.0000 virtualserver_reserved_slots=0 virtualserver_status=template virtualserver_unique_identifier virtualserver_upload_quota=18446744073709551615 virtualserver_weblist_enabled=1 virtualserver_welcomemessage=Welcome\sto\sTeamSpeak,\scheck\s[URL]www.teamspeak.com[\/URL]\sfor\slatest\sinfos.`,
|
||||
"servercreate": `sid=2 virtualserver_port=9988 token=eKnFZQ9EK7G7MhtuQB6+N2B1PNZZ6OZL3ycDp2OW`,
|
||||
"serveridgetbyport": `server_id=1`,
|
||||
"servergrouplist": `sgid=1 name=Guest\sServer\sQuery type=2 iconid=0 savedb=0 sortid=0 namemode=0 n_modifyp=0 n_member_addp=0 n_member_removep=0|sgid=2 name=Admin\sServer\sQuery type=2 iconid=500 savedb=1 sortid=0 namemode=0 n_modifyp=100 n_member_addp=100 n_member_removep=100`,
|
||||
"privilegekeylist": `token=zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX token_type=0 token_id1=17395 token_id2=0 token_created=1499948005 token_description`,
|
||||
"privilegekeyadd": `token=zTfamFVhiMEzhTl49KrOVYaMilHPgQEBQOJFh6qX`,
|
||||
"serverdelete": "",
|
||||
"serverstop": "",
|
||||
"serverstart": "",
|
||||
"serveredit": "",
|
||||
cmdQuit: "",
|
||||
}
|
||||
)
|
||||
|
||||
// newLockListener creates a new listener on the local IP.
|
||||
// If listen fails it panics
|
||||
func newLocalListener() (net.Listener, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// server is a mock TeamSpeak 3 server
|
||||
type server struct {
|
||||
Addr string
|
||||
Listener net.Listener
|
||||
|
||||
t *testing.T
|
||||
conns map[net.Conn]struct{}
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
noHeader bool
|
||||
noBanner bool
|
||||
failConn bool
|
||||
badHeader bool
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
// sconn represents a server connection
|
||||
type sconn struct {
|
||||
id int
|
||||
net.Conn
|
||||
}
|
||||
|
||||
// newServer returns a running server or nil if an error occurred.
|
||||
func newServer(t *testing.T) *server {
|
||||
s := newServerStopped(t)
|
||||
s.Start()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// newServerStopped returns a stopped servers or nil if an error occurred.
|
||||
func newServerStopped(t *testing.T) *server {
|
||||
l, err := newLocalListener()
|
||||
if !assert.NoError(t, err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := &server{
|
||||
Listener: l,
|
||||
conns: make(map[net.Conn]struct{}),
|
||||
done: make(chan struct{}),
|
||||
t: t,
|
||||
}
|
||||
s.Addr = s.Listener.Addr().String()
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the server.
|
||||
func (s *server) Start() {
|
||||
s.wg.Add(1)
|
||||
go s.serve()
|
||||
}
|
||||
|
||||
// server processes incoming requests until signaled to stop with Close.
|
||||
func (s *server) serve() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
conn, err := s.Listener.Accept()
|
||||
if err != nil {
|
||||
if s.running() {
|
||||
assert.NoError(s.t, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
s.conns[conn] = struct{}{}
|
||||
go s.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// writeResponse writes the given msg followed by an error (ok) response.
|
||||
// If msg is empty the only the error (ok) rsponse is sent.
|
||||
func (s *server) writeResponse(c *sconn, msg string) error {
|
||||
if msg != "" {
|
||||
if err := s.write(c.Conn, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.write(c.Conn, errOK)
|
||||
}
|
||||
|
||||
// write writes msg to conn.
|
||||
func (s *server) write(conn net.Conn, msg string) error {
|
||||
_, err := conn.Write([]byte(msg + "\n\r"))
|
||||
if s.running() {
|
||||
assert.NoError(s.t, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// running returns true unless Close has been called, false otherwise.
|
||||
func (s *server) running() bool {
|
||||
select {
|
||||
case <-s.done:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// handle handles a client connection.
|
||||
func (s *server) handle(conn net.Conn) {
|
||||
defer func() {
|
||||
s.closeConn(conn)
|
||||
s.wg.Done()
|
||||
}()
|
||||
|
||||
if s.failConn {
|
||||
return
|
||||
}
|
||||
|
||||
sc := bufio.NewScanner(bufio.NewReader(conn))
|
||||
sc.Split(bufio.ScanLines)
|
||||
|
||||
if !s.noHeader {
|
||||
if s.badHeader {
|
||||
if err := s.write(conn, "bad"); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := s.write(conn, connectHeader); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !s.noBanner {
|
||||
if err := s.write(conn, banner); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c := &sconn{Conn: conn}
|
||||
for sc.Scan() {
|
||||
l := sc.Text()
|
||||
parts := strings.Split(l, " ")
|
||||
resp, ok := commands[parts[0]]
|
||||
var err error
|
||||
if ok {
|
||||
err = s.writeResponse(c, resp)
|
||||
} else if parts[0] == "disconnect" {
|
||||
return
|
||||
} else {
|
||||
err = s.write(c, errUnknownCmd)
|
||||
}
|
||||
if err != nil || parts[0] == cmdQuit {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := sc.Err(); err != nil && s.running() {
|
||||
assert.NoError(s.t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// closeConn closes a client connection and removes it from our map of connections.
|
||||
func (s *server) closeConn(conn net.Conn) {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
conn.Close() // nolint: errcheck
|
||||
delete(s.conns, conn)
|
||||
}
|
||||
|
||||
// Close cleanly shuts down the server.
|
||||
func (s *server) Close() error {
|
||||
close(s.done)
|
||||
err := s.Listener.Close()
|
||||
s.mtx.Lock()
|
||||
for c := range s.conns {
|
||||
if err2 := c.Close(); err2 != nil && err == nil {
|
||||
err = err2
|
||||
}
|
||||
}
|
||||
s.mtx.Unlock()
|
||||
s.wg.Wait()
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScanLines is a split function for a bytes.Scanner that returns each line of
|
||||
// text, stripped of any trailing end-of-line marker. The returned line may
|
||||
// be empty. The end-of-line marker is one newline followed by a carriage return.
|
||||
// In regular expression notation, it is `\n\r`.
|
||||
// The last non-empty line of input will be returned even if it has no newline.
|
||||
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := strings.Index(string(data), "\n\r"); i >= 0 {
|
||||
// We have a full end-of-line terminated line.
|
||||
return i + 2, data[0:i], nil
|
||||
}
|
||||
|
||||
// If we're at EOF, we have a final, non-terminated line. Return it.
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScanLines(t *testing.T) {
|
||||
str := "line1\n\rline2\n\rline3"
|
||||
|
||||
s := bufio.NewScanner(strings.NewReader(str))
|
||||
s.Split(ScanLines)
|
||||
|
||||
lines := []string{
|
||||
"line1",
|
||||
"line2",
|
||||
"line3",
|
||||
}
|
||||
|
||||
var i int
|
||||
for s.Scan() {
|
||||
if !assert.True(t, len(lines) > i) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, lines[i], s.Text())
|
||||
i++
|
||||
}
|
||||
|
||||
assert.Equal(t, len(lines), i)
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package ts3
|
||||
|
||||
// ServerMethods groups server methods.
|
||||
type ServerMethods struct {
|
||||
*Client
|
||||
}
|
||||
|
||||
// Server represents a TeamSpeak 3 virtual server.
|
||||
type Server struct {
|
||||
AnitFloodPointsNeededCommandBlock int `ms:"virtualserver_antiflood_points_needed_command_block"`
|
||||
AnitFloodPointsNeededIPBlock int `ms:"virtualserver_antiflood_points_needed_ip_block"`
|
||||
AnitFloodPointsTickReduce int `ms:"virtualserver_antiflood_points_tick_reduce"`
|
||||
ChannelsOnline int `ms:"virtualserver_channelsonline"`
|
||||
ClientsOnline int `ms:"virtualserver_clientsonline"`
|
||||
CodecEncryptionMode int `ms:"virtualserver_codec_encryption_mode"`
|
||||
ComplainAutoBanCount int `ms:"virtualserver_complain_autoban_count"`
|
||||
ComplainAutoBanTime int `ms:"virtualserver_complain_autoban_time"`
|
||||
ComplainRemoveTime int `ms:"virtualserver_complain_remove_time"`
|
||||
Created int `ms:"virtualserver_created"`
|
||||
DefaultChannelAdminGroup int `ms:"virtualserver_default_channel_admin_group"`
|
||||
DefaultChannelGroup int `ms:"virtualserver_default_channel_group"`
|
||||
DefaultServerGroup int `ms:"virtualserver_default_server_group"`
|
||||
HostBannerGFXInterval int `ms:"virtualserver_hostbanner_gfx_interval"`
|
||||
HostMessageMode int `ms:"virtualserver_hostmessage_mode"`
|
||||
ID int `ms:"virtualserver_id"`
|
||||
MachineID int `ms:"virtualserver_machine_id"`
|
||||
MaxClients int `ms:"virtualserver_maxclients"`
|
||||
MinClientsInChannelBeforeForcedSilence int `ms:"virtualserver_min_clients_in_channel_before_forced_silence"`
|
||||
NeededIdentitySecurityLevel int `ms:"virtualserver_needed_identity_security_level"`
|
||||
Port int `ms:"virtualserver_port"`
|
||||
QueryClientsOnline int `ms:"virtualserver_queryclientsonline"`
|
||||
Uptime int `ms:"virtualserver_uptime"` // TODO(steve): use time.Duration
|
||||
AskForPrivilegeKey bool `ms:"virtualserver_ask_for_privilegekey"`
|
||||
AutoStart bool `ms:"virtualserver_autostart"`
|
||||
FlagPassword bool `ms:"virtualserver_flag_password"`
|
||||
LogChannel bool `ms:"virtualserver_log_channel"`
|
||||
LogClient bool `ms:"virtualserver_log_client"`
|
||||
LogFileTransfer bool `ms:"virtualserver_log_filetransfer"`
|
||||
LogPermissions bool `ms:"virtualserver_log_permissions"`
|
||||
LogQuery bool `ms:"virtualserver_log_client"`
|
||||
LogServer bool `ms:"virtualserver_log_server"`
|
||||
WebListEnabled bool `ms:"virtualserver_web_list_enabled"`
|
||||
PrioritySpeakerDimmModificator float32 `ms:"virtualserver_priority_speaker_dimm_modificator"`
|
||||
BandwidthReceivedLastMinuteTotal int `ms:"virtualserver_bandwidth_received_last_minute_total"`
|
||||
BandwidthReceivedLastSecondTotal int `ms:"virtualserver_bandwidth_received_last_second_total"`
|
||||
BandwidthSentLastMinuteTotal int `ms:"virtualserver_bandwidth_sent_last_minute_total"`
|
||||
BandwidthSentLastSecondTotal int `ms:"virtualserver_bandwidth_sent_last_second_total"`
|
||||
ChannelTempDeleteDelayDefault int `ms:"virtualserver_channel_temp_delete_delay_default"`
|
||||
HostBannerMode int `ms:"virtualserver_hostbanner_mode"`
|
||||
IconID int `ms:"virtualserver_icon_id"`
|
||||
MinAndroidVersion int `ms:"virtualserver_min_android_version"`
|
||||
MinClientVersion int `ms:"virtualserver_min_client_version"`
|
||||
MiniOSVersion int `ms:"virtualserver_min_ios_version"`
|
||||
ReserverSlots int `ms:"virtualserver_reserved_slots"`
|
||||
TotalPing int `ms:"virtualserver_total_ping"`
|
||||
MaxDownloadTotalBandwidth uint64 `ms:"virtualserver_max_download_total_bandwidth"`
|
||||
MaxUploadTotalBandwidth uint64 `ms:"virtualserver_max_upload_total_bandwidth"`
|
||||
MonthBytesDownloaded int64 `ms:"virtualserver_MonthBytesDownloaded"`
|
||||
MonthBytesUploaded int64 `ms:"virtualserver_MonthBytesUploaded"`
|
||||
TotalBytesDownloaded int64 `ms:"virtualserver_TotalBytesDownloaded"`
|
||||
TotalBytesUploaded int64 `ms:"virtualserver_TotalBytesUploaded"`
|
||||
TotalPacketLossControl int64 `ms:"virtualserver_total_packet_loss_control"`
|
||||
TotalPacketLossKeepalive int64 `ms:"virtualserver_total_packet_loss_keepalive"`
|
||||
TotalPacketLossSpeech int64 `ms:"virtualserver_total_packet_loss_speech"`
|
||||
TotalPacketLossTotal int64 `ms:"virtualserver_total_packet_loss_total"`
|
||||
VirtualServerDownloadQuota int64 `ms:"virtualserver_virtualserver_download_quota"`
|
||||
VirtualServerUploadQuota int64 `ms:"virtualserver_virtualserver_Upload_quota"`
|
||||
FileBase string `ms:"virtualserver_filebase"`
|
||||
HostBannerGFXURL string `ms:"virtualserver_hostbanner_gfx_url"`
|
||||
HostBannerURL string `ms:"virtualserver_hostbanner_url"`
|
||||
HostButtonGFXURL string `ms:"virtualserver_hostbutton_gfx_url"`
|
||||
HostButtonToolTip string `ms:"virtualserver_hostbutton_tooltip"`
|
||||
HostButtonURL string `ms:"virtualserver_hostbutton_url"`
|
||||
HostMessage string `ms:"virtualserver_hostmessage"`
|
||||
Name string `ms:"virtualserver_name"`
|
||||
NamePhonetic string `ms:"virtualserver_name_phonetic"`
|
||||
Password string `ms:"virtualserver_password"`
|
||||
Platform string `ms:"virtualserver_platform"`
|
||||
Status string `ms:"virtualserver_status"`
|
||||
UniqueIdentifier string `ms:"virtualserver_unique_identifier"`
|
||||
Version string `ms:"virtualserver_version"`
|
||||
WelcomeMessage string `ms:"virtualserver_welcomemessage"`
|
||||
}
|
||||
|
||||
// List lists virtual servers.
|
||||
func (s *ServerMethods) List(options ...string) ([]*Server, error) {
|
||||
var servers []*Server
|
||||
if _, err := s.ExecCmd(NewCmd("serverlist").WithOptions(options...).WithResponse(&servers)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// IDGetByPort returns the database id of the virtual server running on UDP port.
|
||||
func (s *ServerMethods) IDGetByPort(port uint16) (int, error) {
|
||||
r := struct {
|
||||
ID int `ms:"server_id"`
|
||||
}{}
|
||||
_, err := s.ExecCmd(NewCmd("serveridgetbyport").WithArgs(NewArg("virtualserver_port", port)).WithResponse(&r))
|
||||
return r.ID, err
|
||||
}
|
||||
|
||||
// Info returns detailed configuration information about the selected server.
|
||||
func (s *ServerMethods) Info() (*Server, error) {
|
||||
r := &Server{}
|
||||
if _, err := s.ExecCmd(NewCmd("serverinfo").WithResponse(&r)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Edit changes the selected virtual servers configuration using the given args.
|
||||
func (s *ServerMethods) Edit(args ...CmdArg) error {
|
||||
_, err := s.ExecCmd(NewCmd("serveredit").WithArgs(args...))
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete deletes the virtual server specified by id.
|
||||
// Only virtual server in a stopped state can be deleted.
|
||||
func (s *ServerMethods) Delete(id int) error {
|
||||
_, err := s.ExecCmd(NewCmd("serverdelete").WithArgs(NewArg("sid", id)))
|
||||
return err
|
||||
}
|
||||
|
||||
// CreatedServer is the details returned by a server create.
|
||||
type CreatedServer struct {
|
||||
ID int `ms:"sid"`
|
||||
Port uint16 `ms:"virtualserver_port"`
|
||||
Token string
|
||||
}
|
||||
|
||||
// Create creates a new virtual server using the given properties and returns
|
||||
// its ID, port and initial administrator privilege key.
|
||||
// If virtualserver_port arg is not specified, the server will use the first unused
|
||||
// UDP port.
|
||||
func (s *ServerMethods) Create(name string, args ...CmdArg) (*CreatedServer, error) {
|
||||
r := &CreatedServer{}
|
||||
args = append(args, NewArg("virtualserver_name", name))
|
||||
if _, err := s.ExecCmd(NewCmd("servercreate").WithArgs(args...).WithResponse(r)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Start starts the virtual server specified by id.
|
||||
func (s *ServerMethods) Start(id int) error {
|
||||
_, err := s.ExecCmd(NewCmd("serverstart").WithArgs(NewArg("sid", id)))
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop stops the virtual server specified by id.
|
||||
func (s *ServerMethods) Stop(id int) error {
|
||||
_, err := s.ExecCmd(NewCmd("serverstop").WithArgs(NewArg("sid", id)))
|
||||
return err
|
||||
}
|
||||
|
||||
// Group represents a virtual server group.
|
||||
type Group struct {
|
||||
ID int `ms:"sgid"`
|
||||
Name string
|
||||
Type int
|
||||
IconID int
|
||||
Saved bool `ms:"savedb"`
|
||||
SortID int
|
||||
NameMode int
|
||||
ModifyPower int `ms:"n_modifyp"`
|
||||
MemberAddPower int `ms:"n_member_addp"`
|
||||
MemberRemovePower int `ms:"n_member_addp"`
|
||||
}
|
||||
|
||||
// GroupList returns a list of available groups for the selected server.
|
||||
func (s *ServerMethods) GroupList() ([]*Group, error) {
|
||||
var groups []*Group
|
||||
if _, err := s.ExecCmd(NewCmd("servergrouplist").WithResponse(&groups)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// PrivilegeKey represents a server privilege key.
|
||||
type PrivilegeKey struct {
|
||||
Token string
|
||||
Type int `ms:"token_type"`
|
||||
ID1 int `ms:"token_id1"`
|
||||
ID2 int `ms:"token_id2"`
|
||||
Created int `ms:"token_created"`
|
||||
Description string `ms:"token_description"`
|
||||
}
|
||||
|
||||
// PrivilegeKeyList returns a list of available privilege keys for the selected server,
|
||||
// including their type and group IDs.
|
||||
func (s *ServerMethods) PrivilegeKeyList() ([]*PrivilegeKey, error) {
|
||||
var keys []*PrivilegeKey
|
||||
if _, err := s.ExecCmd(NewCmd("privilegekeylist").WithResponse(&keys)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// PrivilegeKeyAdd creates a new privilege token to the selected server and returns it.
|
||||
// If tokentype is set to 0, the ID specified with id1 will be a server group ID.
|
||||
// Otherwise, id1 is used as a channel group ID and you need to provide a valid channel ID using id2.
|
||||
func (s *ServerMethods) PrivilegeKeyAdd(ttype, id1, id2 int, options ...CmdArg) (string, error) {
|
||||
t := struct {
|
||||
Token string
|
||||
}{}
|
||||
options = append(options, NewArg("tokentype", ttype), NewArg("tokenid1", id1), NewArg("tokenid2", id2))
|
||||
_, err := s.ExecCmd(NewCmd("privilegekeylist").WithArgs(options...).WithResponse(&t))
|
||||
return t.Token, err
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
package ts3
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCmdsServer(t *testing.T) {
|
||||
s := newServer(t)
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
assert.NoError(t, s.Close())
|
||||
}()
|
||||
|
||||
c, err := NewClient(s.Addr, Timeout(time.Second*2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
assert.NoError(t, c.Close())
|
||||
}()
|
||||
|
||||
list := func(t *testing.T) {
|
||||
servers, err := c.Server.List()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected := []*Server{
|
||||
&Server{
|
||||
ID: 1,
|
||||
Port: 10677,
|
||||
Status: "online",
|
||||
ClientsOnline: 1,
|
||||
QueryClientsOnline: 1,
|
||||
MaxClients: 35,
|
||||
Uptime: 12345025,
|
||||
Name: "Server #1",
|
||||
AutoStart: true,
|
||||
MachineID: 1,
|
||||
UniqueIdentifier: "uniq1",
|
||||
},
|
||||
&Server{
|
||||
ID: 2,
|
||||
Port: 10617,
|
||||
Status: "online",
|
||||
ClientsOnline: 3,
|
||||
QueryClientsOnline: 2,
|
||||
MaxClients: 10,
|
||||
Uptime: 3165117,
|
||||
Name: "Server #2",
|
||||
AutoStart: true,
|
||||
MachineID: 1,
|
||||
UniqueIdentifier: "uniq2",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, servers)
|
||||
}
|
||||
|
||||
idgetbyport := func(t *testing.T) {
|
||||
id, err := c.Server.IDGetByPort(1234)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, 1, id)
|
||||
}
|
||||
|
||||
info := func(t *testing.T) {
|
||||
s, err := c.Server.Info()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected := &Server{
|
||||
Status: "template",
|
||||
MaxClients: 32,
|
||||
Name: "Test Server",
|
||||
AnitFloodPointsNeededCommandBlock: 150,
|
||||
AnitFloodPointsNeededIPBlock: 250,
|
||||
AnitFloodPointsTickReduce: 5,
|
||||
ComplainAutoBanCount: 5,
|
||||
ComplainAutoBanTime: 1200,
|
||||
ComplainRemoveTime: 3600,
|
||||
DefaultChannelAdminGroup: 1,
|
||||
DefaultChannelGroup: 4,
|
||||
DefaultServerGroup: 5,
|
||||
MinClientsInChannelBeforeForcedSilence: 100,
|
||||
NeededIdentitySecurityLevel: 8,
|
||||
LogPermissions: true,
|
||||
PrioritySpeakerDimmModificator: -18,
|
||||
MaxDownloadTotalBandwidth: 18446744073709551615,
|
||||
MaxUploadTotalBandwidth: 18446744073709551615,
|
||||
FileBase: "files",
|
||||
HostButtonToolTip: "Multiplay Game Servers",
|
||||
HostButtonURL: "http://www.multiplaygameservers.com",
|
||||
WelcomeMessage: "Welcome to TeamSpeak, check [URL]www.teamspeak.com[/URL] for latest infos.",
|
||||
}
|
||||
assert.Equal(t, expected, s)
|
||||
}
|
||||
|
||||
create := func(t *testing.T) {
|
||||
s, err := c.Server.Create("my server")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
expected := &CreatedServer{
|
||||
ID: 2,
|
||||
Port: 9988,
|
||||
Token: "eKnFZQ9EK7G7MhtuQB6+N2B1PNZZ6OZL3ycDp2OW",
|
||||
}
|
||||
assert.Equal(t, expected, s)
|
||||
}
|
||||
|
||||
edit := func(t *testing.T) {
|
||||
assert.NoError(t, c.Server.Edit(NewArg("virtualserver_maxclients", 10)))
|
||||
}
|
||||
|
||||
del := func(t *testing.T) {
|
||||
assert.NoError(t, c.Server.Delete(1))
|
||||
}
|
||||
|
||||
start := func(t *testing.T) {
|
||||
assert.NoError(t, c.Server.Start(1))
|
||||
}
|
||||
|
||||
stop := func(t *testing.T) {
|
||||
assert.NoError(t, c.Server.Stop(1))
|
||||
}
|
||||
|
||||
grouplist := func(t *testing.T) {
|
||||
groups, err := c.Server.GroupList()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := []*Group{
|
||||
&Group{
|
||||
ID: 1,
|
||||
Name: "Guest Server Query",
|
||||
Type: 2,
|
||||
},
|
||||
&Group{
|
||||
ID: 2,
|
||||
Name: "Admin Server Query",
|
||||
Type: 2,
|
||||
IconID: 500,
|
||||
Saved: true,
|
||||
ModifyPower: 100,
|
||||
MemberAddPower: 100,
|
||||
MemberRemovePower: 100,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, groups)
|
||||
}
|
||||
|
||||
privilegekeylist := func(t *testing.T) {
|
||||
keys, err := c.Server.PrivilegeKeyList()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expected := []*PrivilegeKey{
|
||||
&PrivilegeKey{
|
||||
Token: "zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX",
|
||||
ID1: 17395,
|
||||
Created: 1499948005,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, keys)
|
||||
}
|
||||
|
||||
privilegekeyadd := func(t *testing.T) {
|
||||
token, err := c.Server.PrivilegeKeyAdd(0, 17395, 0)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, "zTfamFVhiMEzhTl49KrOVYaMilHPDQEBQOJFh6qX", token)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
f func(t *testing.T)
|
||||
}{
|
||||
{"list", list},
|
||||
{"idgetbyport", idgetbyport},
|
||||
{"info", info},
|
||||
{"create", create},
|
||||
{"edit", edit},
|
||||
{"del", del},
|
||||
{"start", start},
|
||||
{"stop", stop},
|
||||
{"grouplist", grouplist},
|
||||
{"privilegekeylist", privilegekeylist},
|
||||
{"privilegekeyadd", privilegekeyadd},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, tc.f)
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче