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