]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Remove unused var
[rbdr/mobius] / hotline / client.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
4 "bytes"
5 "embed"
6 "encoding/binary"
7 "errors"
8 "fmt"
6988a057
JH
9 "github.com/gdamore/tcell/v2"
10 "github.com/rivo/tview"
11 "github.com/stretchr/testify/mock"
12 "go.uber.org/zap"
13 "gopkg.in/yaml.v2"
6988a057
JH
14 "math/big"
15 "math/rand"
16 "net"
17 "os"
18 "strings"
19 "time"
20)
21
4f3c459c
JH
22const (
23 trackerListPage = "trackerList"
24)
6988a057 25
22c599ab 26//go:embed banners/*.txt
6988a057
JH
27var bannerDir embed.FS
28
29type Bookmark struct {
30 Name string `yaml:"Name"`
31 Addr string `yaml:"Addr"`
32 Login string `yaml:"Login"`
33 Password string `yaml:"Password"`
34}
35
36type ClientPrefs struct {
37 Username string `yaml:"Username"`
38 IconID int `yaml:"IconID"`
39 Bookmarks []Bookmark `yaml:"Bookmarks"`
4f3c459c 40 Tracker string `yaml:"Tracker"`
6988a057
JH
41}
42
f7e36225
JH
43func (cp *ClientPrefs) IconBytes() []byte {
44 iconBytes := make([]byte, 2)
45 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
46 return iconBytes
47}
48
da1e0d79
JH
49func (cp *ClientPrefs) AddBookmark(name, addr, login, pass string) error {
50 cp.Bookmarks = append(cp.Bookmarks, Bookmark{Addr: addr, Login: login, Password: pass})
51
52 return nil
53}
54
6988a057
JH
55func readConfig(cfgPath string) (*ClientPrefs, error) {
56 fh, err := os.Open(cfgPath)
57 if err != nil {
58 return nil, err
59 }
60
61 prefs := ClientPrefs{}
62 decoder := yaml.NewDecoder(fh)
63 decoder.SetStrict(true)
64 if err := decoder.Decode(&prefs); err != nil {
65 return nil, err
66 }
67 return &prefs, nil
68}
69
70type Client struct {
95753255 71 cfgPath string
6988a057
JH
72 DebugBuf *DebugBuffer
73 Connection net.Conn
6988a057
JH
74 Login *[]byte
75 Password *[]byte
6988a057
JH
76 Flags *[]byte
77 ID *[]byte
78 Version []byte
79 UserAccess []byte
43ecc0f4 80 filePath []string
6988a057
JH
81 UserList []User
82 Logger *zap.SugaredLogger
83 activeTasks map[uint32]*Transaction
84
85 pref *ClientPrefs
86
87 Handlers map[uint16]clientTHandler
88
89 UI *UI
90
91 outbox chan *Transaction
92 Inbox chan *Transaction
93}
94
b198b22b
JH
95func NewClient(cfgPath string, logger *zap.SugaredLogger) *Client {
96 c := &Client{
97 cfgPath: cfgPath,
98 Logger: logger,
99 activeTasks: make(map[uint32]*Transaction),
100 Handlers: clientHandlers,
6988a057 101 }
b198b22b 102 c.UI = NewUI(c)
6988a057 103
b198b22b 104 prefs, err := readConfig(cfgPath)
6988a057 105 if err != nil {
43ecc0f4
JH
106 fmt.Printf("unable to read config file %s", cfgPath)
107 os.Exit(1)
6988a057 108 }
b198b22b 109 c.pref = prefs
6988a057 110
b198b22b 111 return c
6988a057
JH
112}
113
6988a057
JH
114// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
115type DebugBuffer struct {
116 TextView *tview.TextView
117}
118
119func (db *DebugBuffer) Write(p []byte) (int, error) {
120 return db.TextView.Write(p)
121}
122
123// Sync is a noop function that exists to satisfy the zapcore.WriteSyncer interface
124func (db *DebugBuffer) Sync() error {
125 return nil
126}
127
6988a057
JH
128func randomBanner() string {
129 rand.Seed(time.Now().UnixNano())
130
ce348eb8
JH
131 bannerFiles, _ := bannerDir.ReadDir("banners")
132 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
133
134 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
135}
136
6988a057
JH
137type clientTransaction struct {
138 Name string
139 Handler func(*Client, *Transaction) ([]Transaction, error)
140}
141
142func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
143 return ch.Handler(cc, t)
144}
145
146type clientTHandler interface {
147 Handle(*Client, *Transaction) ([]Transaction, error)
148}
149
150type mockClientHandler struct {
151 mock.Mock
152}
153
154func (mh *mockClientHandler) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
155 args := mh.Called(cc, t)
156 return args.Get(0).([]Transaction), args.Error(1)
157}
158
159var clientHandlers = map[uint16]clientTHandler{
160 // Server initiated
161 tranChatMsg: clientTransaction{
162 Name: "tranChatMsg",
163 Handler: handleClientChatMsg,
164 },
165 tranLogin: clientTransaction{
166 Name: "tranLogin",
167 Handler: handleClientTranLogin,
168 },
169 tranShowAgreement: clientTransaction{
170 Name: "tranShowAgreement",
171 Handler: handleClientTranShowAgreement,
172 },
173 tranUserAccess: clientTransaction{
174 Name: "tranUserAccess",
175 Handler: handleClientTranUserAccess,
176 },
177 tranGetUserNameList: clientTransaction{
178 Name: "tranGetUserNameList",
179 Handler: handleClientGetUserNameList,
180 },
181 tranNotifyChangeUser: clientTransaction{
182 Name: "tranNotifyChangeUser",
183 Handler: handleNotifyChangeUser,
184 },
185 tranNotifyDeleteUser: clientTransaction{
186 Name: "tranNotifyDeleteUser",
187 Handler: handleNotifyDeleteUser,
188 },
189 tranGetMsgs: clientTransaction{
190 Name: "tranNotifyDeleteUser",
191 Handler: handleGetMsgs,
192 },
43ecc0f4
JH
193 tranGetFileNameList: clientTransaction{
194 Name: "tranGetFileNameList",
195 Handler: handleGetFileNameList,
196 },
3d2bd095
JH
197 tranServerMsg: clientTransaction{
198 Name: "tranServerMsg",
199 Handler: handleTranServerMsg,
200 },
201}
202
203func handleTranServerMsg(c *Client, t *Transaction) (res []Transaction, err error) {
204 time := time.Now().Format(time.RFC850)
205
206 msg := strings.ReplaceAll(string(t.GetField(fieldData).Data), "\r", "\n")
5c34f875 207 msg += "\n\nAt " + time
3d2bd095
JH
208 title := fmt.Sprintf("| Private Message From: %s |", t.GetField(fieldUserName).Data)
209
210 msgBox := tview.NewTextView().SetScrollable(true)
211 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkSlateBlue)
212 msgBox.SetTitle(title).SetBorder(true)
213 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
214 switch event.Key() {
215 case tcell.KeyEscape:
216 c.UI.Pages.RemovePage("serverMsgModal" + time)
217 }
218 return event
219 })
220
221 centeredFlex := tview.NewFlex().
222 AddItem(nil, 0, 1, false).
223 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
224 AddItem(nil, 0, 1, false).
225 AddItem(msgBox, 0, 2, true).
226 AddItem(nil, 0, 1, false), 0, 2, true).
227 AddItem(nil, 0, 1, false)
228
5c34f875 229 c.UI.Pages.AddPage("serverMsgModal"+time, centeredFlex, true, true)
3d2bd095
JH
230 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
231
232 return res, err
43ecc0f4
JH
233}
234
235func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
236 fTree := tview.NewTreeView().SetTopLevel(1)
237 root := tview.NewTreeNode("Root")
238 fTree.SetRoot(root).SetCurrentNode(root)
239 fTree.SetBorder(true).SetTitle("| Files |")
240 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
241 switch event.Key() {
242 case tcell.KeyEscape:
243 c.UI.Pages.RemovePage("files")
244 c.filePath = []string{}
245 case tcell.KeyEnter:
246 selectedNode := fTree.GetCurrentNode()
247
248 if selectedNode.GetText() == "<- Back" {
249 c.filePath = c.filePath[:len(c.filePath)-1]
250 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
251
252 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
253 c.UI.HLClient.Logger.Errorw("err", "err", err)
254 }
255 return event
256 }
257
258 entry := selectedNode.GetReference().(*FileNameWithInfo)
259
260 if bytes.Equal(entry.Type, []byte("fldr")) {
261 c.Logger.Infow("get new directory listing", "name", string(entry.Name))
262
263 c.filePath = append(c.filePath, string(entry.Name))
264 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
265
266 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
267 c.UI.HLClient.Logger.Errorw("err", "err", err)
268 }
269 } else {
270 // TODO: initiate file download
271 c.Logger.Infow("download file", "name", string(entry.Name))
272 }
273 }
274
275 return event
276 })
277
278 if len(c.filePath) > 0 {
279 node := tview.NewTreeNode("<- Back")
280 root.AddChild(node)
281 }
282
283 var fileList []FileNameWithInfo
284 for _, f := range t.Fields {
285 var fn FileNameWithInfo
286 _, _ = fn.Read(f.Data)
287 fileList = append(fileList, fn)
288
289 if bytes.Equal(fn.Type, []byte("fldr")) {
290 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.Name))
291 node.SetReference(&fn)
292 root.AddChild(node)
293 } else {
294 size := binary.BigEndian.Uint32(fn.FileSize) / 1024
295
4c3b4c7f 296 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.Name, size))
43ecc0f4
JH
297 node.SetReference(&fn)
298 root.AddChild(node)
299 }
300
301 }
302
303 centerFlex := tview.NewFlex().
304 AddItem(nil, 0, 1, false).
305 AddItem(tview.NewFlex().
306 SetDirection(tview.FlexRow).
307 AddItem(nil, 0, 1, false).
308 AddItem(fTree, 20, 1, true).
246ed3a1 309 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
310 AddItem(nil, 0, 1, false)
311
312 c.UI.Pages.AddPage("files", centerFlex, true, true)
313 c.UI.App.Draw()
314
315 return res, err
6988a057
JH
316}
317
318func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
319 newsText := string(t.GetField(fieldData).Data)
320 newsText = strings.ReplaceAll(newsText, "\r", "\n")
321
322 newsTextView := tview.NewTextView().
323 SetText(newsText).
324 SetDoneFunc(func(key tcell.Key) {
325 c.UI.Pages.SwitchToPage("serverUI")
326 c.UI.App.SetFocus(c.UI.chatInput)
327 })
328 newsTextView.SetBorder(true).SetTitle("News")
329
330 c.UI.Pages.AddPage("news", newsTextView, true, true)
43ecc0f4
JH
331 //c.UI.Pages.SwitchToPage("news")
332 //c.UI.App.SetFocus(newsTextView)
6988a057
JH
333 c.UI.App.Draw()
334
335 return res, err
336}
337
338func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
339 newUser := User{
340 ID: t.GetField(fieldUserID).Data,
341 Name: string(t.GetField(fieldUserName).Data),
342 Icon: t.GetField(fieldUserIconID).Data,
343 Flags: t.GetField(fieldUserFlags).Data,
344 }
345
346 // Possible cases:
347 // user is new to the server
348 // user is already on the server but has a new name
349
350 var oldName string
351 var newUserList []User
352 updatedUser := false
353 for _, u := range c.UserList {
354 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
355 if bytes.Equal(newUser.ID, u.ID) {
356 oldName = u.Name
357 u.Name = newUser.Name
358 if u.Name != newUser.Name {
359 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
360 }
361 updatedUser = true
362 }
363 newUserList = append(newUserList, u)
364 }
365
366 if !updatedUser {
367 newUserList = append(newUserList, newUser)
368 }
369
370 c.UserList = newUserList
371
372 c.renderUserList()
373
374 return res, err
375}
376
377func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
378 exitUser := t.GetField(fieldUserID).Data
379
380 var newUserList []User
381 for _, u := range c.UserList {
382 if !bytes.Equal(exitUser, u.ID) {
383 newUserList = append(newUserList, u)
384 }
385 }
386
387 c.UserList = newUserList
388
389 c.renderUserList()
390
391 return res, err
392}
393
394const readBuffSize = 1024000 // 1KB - TODO: what should this be?
395
396func (c *Client) ReadLoop() error {
397 tranBuff := make([]byte, 0)
398 tReadlen := 0
399 // Infinite loop where take action on incoming client requests until the connection is closed
400 for {
401 buf := make([]byte, readBuffSize)
402 tranBuff = tranBuff[tReadlen:]
403
404 readLen, err := c.Connection.Read(buf)
405 if err != nil {
406 return err
407 }
408 tranBuff = append(tranBuff, buf[:readLen]...)
409
410 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
411 // into a slice of transactions
412 var transactions []Transaction
413 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
414 c.Logger.Errorw("Error handling transaction", "err", err)
415 }
416
417 // iterate over all of the transactions that were parsed from the byte slice and handle them
418 for _, t := range transactions {
419 if err := c.HandleTransaction(&t); err != nil {
420 c.Logger.Errorw("Error handling transaction", "err", err)
421 }
422 }
423 }
424}
425
426func (c *Client) GetTransactions() error {
427 tranBuff := make([]byte, 0)
428 tReadlen := 0
429
430 buf := make([]byte, readBuffSize)
431 tranBuff = tranBuff[tReadlen:]
432
433 readLen, err := c.Connection.Read(buf)
434 if err != nil {
435 return err
436 }
437 tranBuff = append(tranBuff, buf[:readLen]...)
438
439 return nil
440}
441
442func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
443 var users []User
444 for _, field := range t.Fields {
71c56068
JH
445 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
446 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
447 // field type. Probably a good idea to do everywhere.
448 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
449 u, err := ReadUser(field.Data)
450 if err != nil {
451 return res, err
452 }
453 users = append(users, *u)
454 }
6988a057
JH
455 }
456 c.UserList = users
457
458 c.renderUserList()
459
460 return res, err
461}
462
463func (c *Client) renderUserList() {
464 c.UI.userList.Clear()
465 for _, u := range c.UserList {
466 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
467 if flagBitmap.Bit(userFlagAdmin) == 1 {
5dd57308 468 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 469 } else {
5dd57308 470 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 471 }
b198b22b 472 // TODO: fade if user is away
6988a057
JH
473 }
474}
475
476func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 477 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
478
479 return res, err
480}
481
482func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
483 c.UserAccess = t.GetField(fieldUserAccess).Data
484
485 return res, err
486}
487
488func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
489 agreement := string(t.GetField(fieldData).Data)
490 agreement = strings.ReplaceAll(agreement, "\r", "\n")
491
492 c.UI.agreeModal = tview.NewModal().
493 SetText(agreement).
494 AddButtons([]string{"Agree", "Disagree"}).
495 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
496 if buttonIndex == 0 {
497 res = append(res,
498 *NewTransaction(
499 tranAgreed, nil,
500 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 501 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
502 NewField(fieldUserFlags, []byte{0x00, 0x00}),
503 NewField(fieldOptions, []byte{0x00, 0x00}),
504 ),
505 )
6988a057
JH
506 c.UI.Pages.HidePage("agreement")
507 c.UI.App.SetFocus(c.UI.chatInput)
508 } else {
f7e36225 509 _ = c.Disconnect()
6988a057
JH
510 c.UI.Pages.SwitchToPage("home")
511 }
512 },
513 )
514
515 c.Logger.Debug("show agreement page")
516 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
6988a057 517 c.UI.Pages.ShowPage("agreement ")
6988a057 518 c.UI.App.Draw()
b198b22b 519
6988a057
JH
520 return res, err
521}
522
523func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
524 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
525 errMsg := string(t.GetField(fieldError).Data)
526 errModal := tview.NewModal()
527 errModal.SetText(errMsg)
528 errModal.AddButtons([]string{"Oh no"})
529 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
530 c.UI.Pages.RemovePage("errModal")
531 })
532 c.UI.Pages.RemovePage("joinServer")
533 c.UI.Pages.AddPage("errModal", errModal, false, true)
534
535 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
536
537 c.Logger.Error(string(t.GetField(fieldError).Data))
538 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
539 }
540 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
541 c.UI.App.SetFocus(c.UI.chatInput)
542
543 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
544 c.Logger.Errorw("err", "err", err)
545 }
546 return res, err
547}
548
549// JoinServer connects to a Hotline server and completes the login flow
550func (c *Client) JoinServer(address, login, passwd string) error {
551 // Establish TCP connection to server
552 if err := c.connect(address); err != nil {
553 return err
554 }
555
556 // Send handshake sequence
557 if err := c.Handshake(); err != nil {
558 return err
559 }
560
561 // Authenticate (send tranLogin 107)
562 if err := c.LogIn(login, passwd); err != nil {
563 return err
564 }
565
566 return nil
567}
568
569// connect establishes a connection with a Server by sending handshake sequence
570func (c *Client) connect(address string) error {
571 var err error
572 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
573 if err != nil {
574 return err
575 }
576 return nil
577}
578
579var ClientHandshake = []byte{
580 0x54, 0x52, 0x54, 0x50, // TRTP
581 0x48, 0x4f, 0x54, 0x4c, // HOTL
582 0x00, 0x01,
583 0x00, 0x02,
584}
585
586var ServerHandshake = []byte{
587 0x54, 0x52, 0x54, 0x50, // TRTP
588 0x00, 0x00, 0x00, 0x00, // ErrorCode
589}
590
591func (c *Client) Handshake() error {
592 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
593 //Sub-protocol ID 4 User defined
594 //Version 2 1 Currently 1
595 //Sub-version 2 User defined
596 if _, err := c.Connection.Write(ClientHandshake); err != nil {
597 return fmt.Errorf("handshake write err: %s", err)
598 }
599
600 replyBuf := make([]byte, 8)
601 _, err := c.Connection.Read(replyBuf)
602 if err != nil {
603 return err
604 }
605
6988a057
JH
606 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
607 return nil
608 }
6988a057 609
b198b22b 610 // In the case of an error, client and server close the connection.
6988a057
JH
611 return fmt.Errorf("handshake response err: %s", err)
612}
613
614func (c *Client) LogIn(login string, password string) error {
615 return c.Send(
616 *NewTransaction(
617 tranLogin, nil,
618 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 619 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
620 NewField(fieldUserLogin, negateString([]byte(login))),
621 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
622 NewField(fieldVersion, []byte{0, 2}),
623 ),
624 )
625}
626
6988a057
JH
627func (c *Client) Send(t Transaction) error {
628 requestNum := binary.BigEndian.Uint16(t.Type)
629 tID := binary.BigEndian.Uint32(t.ID)
630
631 //handler := TransactionHandlers[requestNum]
632
633 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
634 if t.IsReply == 0 {
635 c.activeTasks[tID] = &t
636 }
637
638 var n int
639 var err error
640 if n, err = c.Connection.Write(t.Payload()); err != nil {
641 return err
642 }
643 c.Logger.Debugw("Sent Transaction",
644 "IsReply", t.IsReply,
645 "type", requestNum,
646 "sentBytes", n,
647 )
648 return nil
649}
650
651func (c *Client) HandleTransaction(t *Transaction) error {
652 var origT Transaction
653 if t.IsReply == 1 {
654 requestID := binary.BigEndian.Uint32(t.ID)
655 origT = *c.activeTasks[requestID]
656 t.Type = origT.Type
657 }
658
659 requestNum := binary.BigEndian.Uint16(t.Type)
660 c.Logger.Infow(
661 "Received Transaction",
662 "RequestType", requestNum,
663 )
664
665 if handler, ok := c.Handlers[requestNum]; ok {
666 outT, _ := handler.Handle(c, t)
667 for _, t := range outT {
668 c.Send(t)
669 }
670 } else {
671 c.Logger.Errorw(
672 "Unimplemented transaction type received",
673 "RequestID", requestNum,
674 "TransactionID", t.ID,
675 )
676 }
677
678 return nil
679}
680
681func (c *Client) Connected() bool {
6988a057
JH
682 // c.Agreed == true &&
683 if c.UserAccess != nil {
684 return true
685 }
686 return false
687}
688
689func (c *Client) Disconnect() error {
690 err := c.Connection.Close()
691 if err != nil {
692 return err
693 }
694 return nil
695}