]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Minor cleanup
[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")
207 msg += "\n\nAt " + time
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
229
230 c.UI.Pages.AddPage("serverMsgModal" + time, centeredFlex, true, true)
231 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
232
233 return res, err
43ecc0f4
JH
234}
235
236func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
237 fTree := tview.NewTreeView().SetTopLevel(1)
238 root := tview.NewTreeNode("Root")
239 fTree.SetRoot(root).SetCurrentNode(root)
240 fTree.SetBorder(true).SetTitle("| Files |")
241 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
242 switch event.Key() {
243 case tcell.KeyEscape:
244 c.UI.Pages.RemovePage("files")
245 c.filePath = []string{}
246 case tcell.KeyEnter:
247 selectedNode := fTree.GetCurrentNode()
248
249 if selectedNode.GetText() == "<- Back" {
250 c.filePath = c.filePath[:len(c.filePath)-1]
251 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
252
253 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
254 c.UI.HLClient.Logger.Errorw("err", "err", err)
255 }
256 return event
257 }
258
259 entry := selectedNode.GetReference().(*FileNameWithInfo)
260
261 if bytes.Equal(entry.Type, []byte("fldr")) {
262 c.Logger.Infow("get new directory listing", "name", string(entry.Name))
263
264 c.filePath = append(c.filePath, string(entry.Name))
265 f := NewField(fieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
266
267 if err := c.UI.HLClient.Send(*NewTransaction(tranGetFileNameList, nil, f)); err != nil {
268 c.UI.HLClient.Logger.Errorw("err", "err", err)
269 }
270 } else {
271 // TODO: initiate file download
272 c.Logger.Infow("download file", "name", string(entry.Name))
273 }
274 }
275
276 return event
277 })
278
279 if len(c.filePath) > 0 {
280 node := tview.NewTreeNode("<- Back")
281 root.AddChild(node)
282 }
283
284 var fileList []FileNameWithInfo
285 for _, f := range t.Fields {
286 var fn FileNameWithInfo
287 _, _ = fn.Read(f.Data)
288 fileList = append(fileList, fn)
289
290 if bytes.Equal(fn.Type, []byte("fldr")) {
291 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.Name))
292 node.SetReference(&fn)
293 root.AddChild(node)
294 } else {
295 size := binary.BigEndian.Uint32(fn.FileSize) / 1024
296
4c3b4c7f 297 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.Name, size))
43ecc0f4
JH
298 node.SetReference(&fn)
299 root.AddChild(node)
300 }
301
302 }
303
304 centerFlex := tview.NewFlex().
305 AddItem(nil, 0, 1, false).
306 AddItem(tview.NewFlex().
307 SetDirection(tview.FlexRow).
308 AddItem(nil, 0, 1, false).
309 AddItem(fTree, 20, 1, true).
246ed3a1 310 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
311 AddItem(nil, 0, 1, false)
312
313 c.UI.Pages.AddPage("files", centerFlex, true, true)
314 c.UI.App.Draw()
315
316 return res, err
6988a057
JH
317}
318
319func handleGetMsgs(c *Client, t *Transaction) (res []Transaction, err error) {
320 newsText := string(t.GetField(fieldData).Data)
321 newsText = strings.ReplaceAll(newsText, "\r", "\n")
322
323 newsTextView := tview.NewTextView().
324 SetText(newsText).
325 SetDoneFunc(func(key tcell.Key) {
326 c.UI.Pages.SwitchToPage("serverUI")
327 c.UI.App.SetFocus(c.UI.chatInput)
328 })
329 newsTextView.SetBorder(true).SetTitle("News")
330
331 c.UI.Pages.AddPage("news", newsTextView, true, true)
43ecc0f4
JH
332 //c.UI.Pages.SwitchToPage("news")
333 //c.UI.App.SetFocus(newsTextView)
6988a057
JH
334 c.UI.App.Draw()
335
336 return res, err
337}
338
339func handleNotifyChangeUser(c *Client, t *Transaction) (res []Transaction, err error) {
340 newUser := User{
341 ID: t.GetField(fieldUserID).Data,
342 Name: string(t.GetField(fieldUserName).Data),
343 Icon: t.GetField(fieldUserIconID).Data,
344 Flags: t.GetField(fieldUserFlags).Data,
345 }
346
347 // Possible cases:
348 // user is new to the server
349 // user is already on the server but has a new name
350
351 var oldName string
352 var newUserList []User
353 updatedUser := false
354 for _, u := range c.UserList {
355 c.Logger.Debugw("Comparing Users", "userToUpdate", newUser.ID, "myID", u.ID, "userToUpdateName", newUser.Name, "myname", u.Name)
356 if bytes.Equal(newUser.ID, u.ID) {
357 oldName = u.Name
358 u.Name = newUser.Name
359 if u.Name != newUser.Name {
360 _, _ = fmt.Fprintf(c.UI.chatBox, " <<< "+oldName+" is now known as "+newUser.Name+" >>>\n")
361 }
362 updatedUser = true
363 }
364 newUserList = append(newUserList, u)
365 }
366
367 if !updatedUser {
368 newUserList = append(newUserList, newUser)
369 }
370
371 c.UserList = newUserList
372
373 c.renderUserList()
374
375 return res, err
376}
377
378func handleNotifyDeleteUser(c *Client, t *Transaction) (res []Transaction, err error) {
379 exitUser := t.GetField(fieldUserID).Data
380
381 var newUserList []User
382 for _, u := range c.UserList {
383 if !bytes.Equal(exitUser, u.ID) {
384 newUserList = append(newUserList, u)
385 }
386 }
387
388 c.UserList = newUserList
389
390 c.renderUserList()
391
392 return res, err
393}
394
395const readBuffSize = 1024000 // 1KB - TODO: what should this be?
396
397func (c *Client) ReadLoop() error {
398 tranBuff := make([]byte, 0)
399 tReadlen := 0
400 // Infinite loop where take action on incoming client requests until the connection is closed
401 for {
402 buf := make([]byte, readBuffSize)
403 tranBuff = tranBuff[tReadlen:]
404
405 readLen, err := c.Connection.Read(buf)
406 if err != nil {
407 return err
408 }
409 tranBuff = append(tranBuff, buf[:readLen]...)
410
411 // We may have read multiple requests worth of bytes from Connection.Read. readTransactions splits them
412 // into a slice of transactions
413 var transactions []Transaction
414 if transactions, tReadlen, err = readTransactions(tranBuff); err != nil {
415 c.Logger.Errorw("Error handling transaction", "err", err)
416 }
417
418 // iterate over all of the transactions that were parsed from the byte slice and handle them
419 for _, t := range transactions {
420 if err := c.HandleTransaction(&t); err != nil {
421 c.Logger.Errorw("Error handling transaction", "err", err)
422 }
423 }
424 }
425}
426
427func (c *Client) GetTransactions() error {
428 tranBuff := make([]byte, 0)
429 tReadlen := 0
430
431 buf := make([]byte, readBuffSize)
432 tranBuff = tranBuff[tReadlen:]
433
434 readLen, err := c.Connection.Read(buf)
435 if err != nil {
436 return err
437 }
438 tranBuff = append(tranBuff, buf[:readLen]...)
439
440 return nil
441}
442
443func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
444 var users []User
445 for _, field := range t.Fields {
71c56068
JH
446 // The Hotline protocol docs say that ClientGetUserNameList should only return fieldUsernameWithInfo (300)
447 // fields, but shxd sneaks in fieldChatSubject (115) so it's important to filter explicitly for the expected
448 // field type. Probably a good idea to do everywhere.
449 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
450 u, err := ReadUser(field.Data)
451 if err != nil {
452 return res, err
453 }
454 users = append(users, *u)
455 }
6988a057
JH
456 }
457 c.UserList = users
458
459 c.renderUserList()
460
461 return res, err
462}
463
464func (c *Client) renderUserList() {
465 c.UI.userList.Clear()
466 for _, u := range c.UserList {
467 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
468 if flagBitmap.Bit(userFlagAdmin) == 1 {
5dd57308 469 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 470 } else {
5dd57308 471 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 472 }
b198b22b 473 // TODO: fade if user is away
6988a057
JH
474 }
475}
476
477func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 478 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
479
480 return res, err
481}
482
483func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
484 c.UserAccess = t.GetField(fieldUserAccess).Data
485
486 return res, err
487}
488
489func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
490 agreement := string(t.GetField(fieldData).Data)
491 agreement = strings.ReplaceAll(agreement, "\r", "\n")
492
493 c.UI.agreeModal = tview.NewModal().
494 SetText(agreement).
495 AddButtons([]string{"Agree", "Disagree"}).
496 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
497 if buttonIndex == 0 {
498 res = append(res,
499 *NewTransaction(
500 tranAgreed, nil,
501 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 502 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
503 NewField(fieldUserFlags, []byte{0x00, 0x00}),
504 NewField(fieldOptions, []byte{0x00, 0x00}),
505 ),
506 )
6988a057
JH
507 c.UI.Pages.HidePage("agreement")
508 c.UI.App.SetFocus(c.UI.chatInput)
509 } else {
f7e36225 510 _ = c.Disconnect()
6988a057
JH
511 c.UI.Pages.SwitchToPage("home")
512 }
513 },
514 )
515
516 c.Logger.Debug("show agreement page")
517 c.UI.Pages.AddPage("agreement", c.UI.agreeModal, false, true)
6988a057 518 c.UI.Pages.ShowPage("agreement ")
6988a057 519 c.UI.App.Draw()
b198b22b 520
6988a057
JH
521 return res, err
522}
523
524func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
525 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
526 errMsg := string(t.GetField(fieldError).Data)
527 errModal := tview.NewModal()
528 errModal.SetText(errMsg)
529 errModal.AddButtons([]string{"Oh no"})
530 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
531 c.UI.Pages.RemovePage("errModal")
532 })
533 c.UI.Pages.RemovePage("joinServer")
534 c.UI.Pages.AddPage("errModal", errModal, false, true)
535
536 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
537
538 c.Logger.Error(string(t.GetField(fieldError).Data))
539 return nil, errors.New("login error: " + string(t.GetField(fieldError).Data))
540 }
541 c.UI.Pages.AddAndSwitchToPage("serverUI", c.UI.renderServerUI(), true)
542 c.UI.App.SetFocus(c.UI.chatInput)
543
544 if err := c.Send(*NewTransaction(tranGetUserNameList, nil)); err != nil {
545 c.Logger.Errorw("err", "err", err)
546 }
547 return res, err
548}
549
550// JoinServer connects to a Hotline server and completes the login flow
551func (c *Client) JoinServer(address, login, passwd string) error {
552 // Establish TCP connection to server
553 if err := c.connect(address); err != nil {
554 return err
555 }
556
557 // Send handshake sequence
558 if err := c.Handshake(); err != nil {
559 return err
560 }
561
562 // Authenticate (send tranLogin 107)
563 if err := c.LogIn(login, passwd); err != nil {
564 return err
565 }
566
567 return nil
568}
569
570// connect establishes a connection with a Server by sending handshake sequence
571func (c *Client) connect(address string) error {
572 var err error
573 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
574 if err != nil {
575 return err
576 }
577 return nil
578}
579
580var ClientHandshake = []byte{
581 0x54, 0x52, 0x54, 0x50, // TRTP
582 0x48, 0x4f, 0x54, 0x4c, // HOTL
583 0x00, 0x01,
584 0x00, 0x02,
585}
586
587var ServerHandshake = []byte{
588 0x54, 0x52, 0x54, 0x50, // TRTP
589 0x00, 0x00, 0x00, 0x00, // ErrorCode
590}
591
592func (c *Client) Handshake() error {
593 //Protocol ID 4 ‘TRTP’ 0x54 52 54 50
594 //Sub-protocol ID 4 User defined
595 //Version 2 1 Currently 1
596 //Sub-version 2 User defined
597 if _, err := c.Connection.Write(ClientHandshake); err != nil {
598 return fmt.Errorf("handshake write err: %s", err)
599 }
600
601 replyBuf := make([]byte, 8)
602 _, err := c.Connection.Read(replyBuf)
603 if err != nil {
604 return err
605 }
606
6988a057
JH
607 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
608 return nil
609 }
6988a057 610
b198b22b 611 // In the case of an error, client and server close the connection.
6988a057
JH
612 return fmt.Errorf("handshake response err: %s", err)
613}
614
615func (c *Client) LogIn(login string, password string) error {
616 return c.Send(
617 *NewTransaction(
618 tranLogin, nil,
619 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 620 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
621 NewField(fieldUserLogin, negateString([]byte(login))),
622 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
623 NewField(fieldVersion, []byte{0, 2}),
624 ),
625 )
626}
627
6988a057
JH
628func (c *Client) Send(t Transaction) error {
629 requestNum := binary.BigEndian.Uint16(t.Type)
630 tID := binary.BigEndian.Uint32(t.ID)
631
632 //handler := TransactionHandlers[requestNum]
633
634 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
635 if t.IsReply == 0 {
636 c.activeTasks[tID] = &t
637 }
638
639 var n int
640 var err error
641 if n, err = c.Connection.Write(t.Payload()); err != nil {
642 return err
643 }
644 c.Logger.Debugw("Sent Transaction",
645 "IsReply", t.IsReply,
646 "type", requestNum,
647 "sentBytes", n,
648 )
649 return nil
650}
651
652func (c *Client) HandleTransaction(t *Transaction) error {
653 var origT Transaction
654 if t.IsReply == 1 {
655 requestID := binary.BigEndian.Uint32(t.ID)
656 origT = *c.activeTasks[requestID]
657 t.Type = origT.Type
658 }
659
660 requestNum := binary.BigEndian.Uint16(t.Type)
661 c.Logger.Infow(
662 "Received Transaction",
663 "RequestType", requestNum,
664 )
665
666 if handler, ok := c.Handlers[requestNum]; ok {
667 outT, _ := handler.Handle(c, t)
668 for _, t := range outT {
669 c.Send(t)
670 }
671 } else {
672 c.Logger.Errorw(
673 "Unimplemented transaction type received",
674 "RequestID", requestNum,
675 "TransactionID", t.ID,
676 )
677 }
678
679 return nil
680}
681
682func (c *Client) Connected() bool {
6988a057
JH
683 // c.Agreed == true &&
684 if c.UserAccess != nil {
685 return true
686 }
687 return false
688}
689
690func (c *Client) Disconnect() error {
691 err := c.Connection.Close()
692 if err != nil {
693 return err
694 }
695 return nil
696}