Skip to content

Commit a81b447

Browse files
feat: add support for ssh connections (#35)
Add support for connecting to a Teamspeak server over SSH with the new client option SSH(...). This is a more secure than the legacy method so should be used where possible.
1 parent 5ab781c commit a81b447

File tree

9 files changed

+578
-17
lines changed

9 files changed

+578
-17
lines changed

basic_cmds_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,42 @@ func TestCmdsBasic(t *testing.T) {
2020
if !assert.NoError(t, err) {
2121
return
2222
}
23+
defer func() {
24+
assert.NoError(t, c.Close())
25+
}()
26+
27+
testCmdsBasic(t, c)
28+
}
29+
30+
func TestCmdsBasicSSH(t *testing.T) {
31+
s := newServer(t)
32+
if s == nil {
33+
return
34+
}
35+
s.useSSH = true
36+
defer func() {
37+
assert.NoError(t, s.Close())
38+
}()
2339

40+
c, err := NewClient(s.Addr, Timeout(time.Second*2), SSH(sshClientTestConfig))
41+
if !assert.NoError(t, err) {
42+
return
43+
}
2444
defer func() {
2545
assert.NoError(t, c.Close())
2646
}()
2747

48+
testCmdsBasic(t, c)
49+
}
50+
51+
func testCmdsBasic(t *testing.T, c *Client) { //nolint: thelper
2852
auth := func(t *testing.T) {
2953
t.Helper()
30-
if err = c.Login("user", "pass"); !assert.NoError(t, err) {
54+
if err := c.Login("user", "pass"); !assert.NoError(t, err) {
3155
return
3256
}
3357

34-
if err = c.Logout(); !assert.NoError(t, err) {
58+
if err := c.Logout(); !assert.NoError(t, err) {
3559
return
3660
}
3761
}

client.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ import (
99
"regexp"
1010
"strings"
1111
"time"
12+
13+
"golang.org/x/crypto/ssh"
1214
)
1315

1416
const (
15-
// DefaultPort is the default TeamSpeak 3 ServerQuery port.
16-
DefaultPort = 10011
17-
1817
// MaxParseTokenSize is the maximum buffer size used to parse the
1918
// server responses.
2019
// It's relatively large to enable us to deal with the typical responses
@@ -44,9 +43,18 @@ var (
4443
DefaultNotifyBufSize = 5
4544
)
4645

46+
// Connection is a connection to a TeamSpeak 3 server.
47+
// It's a wrapper around net.Conn with a Connect method.
48+
type Connection interface {
49+
net.Conn
50+
51+
// Connect connects to the server on addr with a timeout.
52+
Connect(addr string, timeout time.Duration) error
53+
}
54+
4755
// Client is a TeamSpeak 3 ServerQuery client.
4856
type Client struct {
49-
conn net.Conn
57+
conn Connection
5058
timeout time.Duration
5159
keepAlive time.Duration
5260
scanner *bufio.Scanner
@@ -114,13 +122,29 @@ func ConnectHeader(connectHeader string) func(*Client) error {
114122
}
115123
}
116124

117-
// NewClient returns a new TeamSpeak 3 client connected to addr.
118-
func NewClient(addr string, options ...func(c *Client) error) (*Client, error) {
119-
if !strings.Contains(addr, ":") {
120-
addr = fmt.Sprintf("%v:%v", addr, DefaultPort)
125+
// SSH tells the client to use SSH instead of insecure legacy TCP.
126+
// A valid login has to be provided with ssh.ClientConfig.
127+
//
128+
// Example config (missing host-key validation):
129+
//
130+
// &ssh.ClientConfig{
131+
// User: "serveradmin",
132+
// Auth: []ssh.AuthMethod{
133+
// ssh.Password("password"),
134+
// },
135+
// }
136+
func SSH(config *ssh.ClientConfig) func(*Client) error {
137+
return func(c *Client) error {
138+
c.conn = &sshConnection{config: config}
139+
return nil
121140
}
141+
}
122142

143+
// NewClient returns a new TeamSpeak 3 client connected to addr.
144+
// Use with SSH where possible for improved security.
145+
func NewClient(addr string, options ...func(c *Client) error) (*Client, error) {
123146
c := &Client{
147+
conn: new(legacyConnection),
124148
timeout: DefaultTimeout,
125149
keepAlive: DefaultKeepAlive,
126150
buf: make([]byte, startBufSize),
@@ -145,9 +169,8 @@ func NewClient(addr string, options ...func(c *Client) error) (*Client, error) {
145169
// Wire up command groups
146170
c.Server = &ServerMethods{Client: c}
147171

148-
var err error
149-
if c.conn, err = net.DialTimeout("tcp", addr, c.timeout); err != nil {
150-
return nil, fmt.Errorf("client: dial timeout: %w", err)
172+
if err := c.conn.Connect(addr, c.timeout); err != nil {
173+
return nil, fmt.Errorf("client: connect: %w", err)
151174
}
152175

153176
c.scanner = bufio.NewScanner(bufio.NewReader(c.conn))

client_test.go

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,38 @@ func TestClient(t *testing.T) {
4040
assert.Error(t, err)
4141
}
4242

43+
func TestClientSSH(t *testing.T) {
44+
s := newServer(t)
45+
if s == nil {
46+
return
47+
}
48+
s.useSSH = true
49+
defer func() {
50+
assert.NoError(t, s.Close())
51+
}()
52+
53+
c, err := NewClient(s.Addr, Timeout(time.Second), SSH(sshClientTestConfig))
54+
if !assert.NoError(t, err) {
55+
return
56+
}
57+
58+
defer func() {
59+
assert.Error(t, c.Close())
60+
}()
61+
62+
_, err = c.Exec("version")
63+
assert.NoError(t, err)
64+
65+
_, err = c.ExecCmd(NewCmd("version"))
66+
assert.NoError(t, err)
67+
68+
_, err = c.ExecCmd(NewCmd("invalid"))
69+
assert.Error(t, err)
70+
71+
_, err = c.ExecCmd(NewCmd("disconnect"))
72+
assert.Error(t, err)
73+
}
74+
4375
func TestClientNilOption(t *testing.T) {
4476
_, err := NewClient("", nil)
4577
if !assert.Error(t, err) {
@@ -79,6 +111,27 @@ func TestClientDisconnect(t *testing.T) {
79111
assert.Error(t, err)
80112
}
81113

114+
func TestClientDisconnectSSH(t *testing.T) {
115+
s := newServer(t)
116+
if s == nil {
117+
return
118+
}
119+
s.useSSH = true
120+
defer func() {
121+
assert.NoError(t, s.Close())
122+
}()
123+
124+
c, err := NewClient(s.Addr, Timeout(time.Second), SSH(sshClientTestConfig))
125+
if !assert.NoError(t, err) {
126+
return
127+
}
128+
129+
assert.NoError(t, c.Close())
130+
131+
_, err = c.Exec("version")
132+
assert.Error(t, err)
133+
}
134+
82135
func TestClientWriteFail(t *testing.T) {
83136
s := newServer(t)
84137
if s == nil {
@@ -92,7 +145,7 @@ func TestClientWriteFail(t *testing.T) {
92145
if !assert.NoError(t, err) {
93146
return
94147
}
95-
assert.NoError(t, c.conn.(*net.TCPConn).CloseWrite())
148+
assert.NoError(t, c.conn.(*legacyConnection).Conn.(*net.TCPConn).CloseWrite())
96149

97150
_, err = c.Exec("version")
98151
assert.Error(t, err)
@@ -108,6 +161,16 @@ func TestClientDialFail(t *testing.T) {
108161
assert.NoError(t, c.Close())
109162
}
110163

164+
func TestClientDialFailSSH(t *testing.T) {
165+
c, err := NewClient("127.0.0.1", Timeout(time.Nanosecond), SSH(sshClientTestConfig))
166+
if assert.Error(t, err) {
167+
return
168+
}
169+
170+
// Should never get here
171+
assert.NoError(t, c.Close())
172+
}
173+
111174
func TestClientTimeout(t *testing.T) {
112175
s := newServer(t)
113176
if s == nil {
@@ -127,6 +190,26 @@ func TestClientTimeout(t *testing.T) {
127190
assert.Error(t, err)
128191
}
129192

193+
func TestClientTimeoutSSH(t *testing.T) {
194+
s := newServer(t)
195+
if s == nil {
196+
return
197+
}
198+
s.useSSH = true
199+
defer func() {
200+
assert.NoError(t, s.Close())
201+
}()
202+
203+
c, err := NewClient(s.Addr, Timeout(time.Millisecond*100), SSH(sshClientTestConfig))
204+
if !assert.NoError(t, err) {
205+
return
206+
}
207+
208+
// Not receiving a response must cause a timeout
209+
_, err = c.Exec(" ")
210+
assert.Error(t, err)
211+
}
212+
130213
func TestClientDeadline(t *testing.T) {
131214
s := newServer(t)
132215
if s == nil {
@@ -151,6 +234,31 @@ func TestClientDeadline(t *testing.T) {
151234
assert.NoError(t, err)
152235
}
153236

237+
func TestClientDeadlineSSH(t *testing.T) {
238+
s := newServer(t)
239+
if s == nil {
240+
return
241+
}
242+
s.useSSH = true
243+
defer func() {
244+
assert.NoError(t, s.Close())
245+
}()
246+
247+
c, err := NewClient(s.Addr, Timeout(time.Millisecond*100), SSH(sshClientTestConfig))
248+
if !assert.NoError(t, err) {
249+
return
250+
}
251+
252+
_, err = c.Exec("version")
253+
assert.NoError(t, err)
254+
255+
// Inactivity must not cause a timeout
256+
time.Sleep(c.timeout * 2)
257+
258+
_, err = c.Exec("version")
259+
assert.NoError(t, err)
260+
}
261+
154262
func TestClientNoHeader(t *testing.T) {
155263
s := newServerStopped(t)
156264
if s == nil {
@@ -211,6 +319,26 @@ func TestClientFailConn(t *testing.T) {
211319
assert.NoError(t, c.Close())
212320
}
213321

322+
func TestClientFailConnSSH(t *testing.T) {
323+
s := newServerStopped(t)
324+
if s == nil {
325+
return
326+
}
327+
s.failConn = true
328+
s.Start()
329+
defer func() {
330+
assert.NoError(t, s.Close())
331+
}()
332+
333+
c, err := NewClient(s.Addr, Timeout(time.Second), SSH(sshClientTestConfig))
334+
if assert.Error(t, err) {
335+
return
336+
}
337+
338+
// Should never get here
339+
assert.NoError(t, c.Close())
340+
}
341+
214342
func TestClientBadHeader(t *testing.T) {
215343
s := newServerStopped(t)
216344
if s == nil {

0 commit comments

Comments
 (0)