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