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