]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Make UserFlags public
[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"
6988a057 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 {
89bbc565
JH
38 Username string `yaml:"Username"`
39 IconID int `yaml:"IconID"`
40 Bookmarks []Bookmark `yaml:"Bookmarks"`
41 Tracker string `yaml:"Tracker"`
42 EnableBell bool `yaml:"EnableBell"`
6988a057
JH
43}
44
f7e36225
JH
45func (cp *ClientPrefs) IconBytes() []byte {
46 iconBytes := make([]byte, 2)
47 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
48 return iconBytes
49}
50
48ecca21 51func (cp *ClientPrefs) AddBookmark(name, addr, login, pass string) {
da1e0d79 52 cp.Bookmarks = append(cp.Bookmarks, Bookmark{Addr: addr, Login: login, Password: pass})
da1e0d79
JH
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)
6988a057
JH
63 if err := decoder.Decode(&prefs); err != nil {
64 return nil, err
65 }
66 return &prefs, nil
67}
68
69type Client struct {
95753255 70 cfgPath string
6988a057
JH
71 DebugBuf *DebugBuffer
72 Connection net.Conn
6988a057 73 UserAccess []byte
43ecc0f4 74 filePath []string
6988a057
JH
75 UserList []User
76 Logger *zap.SugaredLogger
77 activeTasks map[uint32]*Transaction
e005c191 78 serverName string
6988a057 79
d005ef04 80 Pref *ClientPrefs
6988a057 81
5978b74f 82 Handlers map[uint16]ClientHandler
6988a057
JH
83
84 UI *UI
85
72dd37f1 86 Inbox chan *Transaction
6988a057
JH
87}
88
5978b74f
JH
89type ClientHandler func(*Client, *Transaction) ([]Transaction, error)
90
91func (c *Client) HandleFunc(transactionID uint16, handler ClientHandler) {
92 c.Handlers[transactionID] = handler
93}
94
d005ef04
JH
95func NewClient(username string, logger *zap.SugaredLogger) *Client {
96 c := &Client{
97 Logger: logger,
98 activeTasks: make(map[uint32]*Transaction),
5978b74f 99 Handlers: make(map[uint16]ClientHandler),
d005ef04
JH
100 }
101 c.Pref = &ClientPrefs{Username: username}
102
103 return c
104}
105
106func NewUIClient(cfgPath string, logger *zap.SugaredLogger) *Client {
b198b22b
JH
107 c := &Client{
108 cfgPath: cfgPath,
109 Logger: logger,
110 activeTasks: make(map[uint32]*Transaction),
111 Handlers: clientHandlers,
6988a057 112 }
b198b22b 113 c.UI = NewUI(c)
6988a057 114
b198b22b 115 prefs, err := readConfig(cfgPath)
6988a057 116 if err != nil {
f4a69647 117 logger.Fatal(fmt.Sprintf("unable to read config file %s\n", cfgPath))
6988a057 118 }
d005ef04 119 c.Pref = prefs
6988a057 120
b198b22b 121 return c
6988a057
JH
122}
123
6988a057
JH
124// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
125type DebugBuffer struct {
126 TextView *tview.TextView
127}
128
129func (db *DebugBuffer) Write(p []byte) (int, error) {
130 return db.TextView.Write(p)
131}
132
7cd900d6 133// Sync is a noop function that dataFile to satisfy the zapcore.WriteSyncer interface
6988a057
JH
134func (db *DebugBuffer) Sync() error {
135 return nil
136}
137
6988a057
JH
138func randomBanner() string {
139 rand.Seed(time.Now().UnixNano())
140
ce348eb8
JH
141 bannerFiles, _ := bannerDir.ReadDir("banners")
142 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
143
144 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
145}
146
d005ef04 147type ClientTransaction struct {
6988a057
JH
148 Name string
149 Handler func(*Client, *Transaction) ([]Transaction, error)
150}
151
d005ef04 152func (ch ClientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
6988a057
JH
153 return ch.Handler(cc, t)
154}
155
d005ef04 156type ClientTHandler interface {
6988a057
JH
157 Handle(*Client, *Transaction) ([]Transaction, error)
158}
159
5978b74f
JH
160var clientHandlers = map[uint16]ClientHandler{
161 TranChatMsg: handleClientChatMsg,
162 TranLogin: handleClientTranLogin,
163 TranShowAgreement: handleClientTranShowAgreement,
164 TranUserAccess: handleClientTranUserAccess,
165 TranGetUserNameList: handleClientGetUserNameList,
166 TranNotifyChangeUser: handleNotifyChangeUser,
167 TranNotifyDeleteUser: handleNotifyDeleteUser,
168 TranGetMsgs: handleGetMsgs,
169 TranGetFileNameList: handleGetFileNameList,
170 TranServerMsg: handleTranServerMsg,
171 TranKeepAlive: func(client *Client, transaction *Transaction) (t []Transaction, err error) {
172 return t, err
9d41bcdf 173 },
3d2bd095
JH
174}
175
176func handleTranServerMsg(c *Client, t *Transaction) (res []Transaction, err error) {
48ecca21 177 now := time.Now().Format(time.RFC850)
3d2bd095 178
d005ef04 179 msg := strings.ReplaceAll(string(t.GetField(FieldData).Data), "\r", "\n")
48ecca21 180 msg += "\n\nAt " + now
d005ef04 181 title := fmt.Sprintf("| Private Message From: %s |", t.GetField(FieldUserName).Data)
3d2bd095
JH
182
183 msgBox := tview.NewTextView().SetScrollable(true)
184 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkSlateBlue)
185 msgBox.SetTitle(title).SetBorder(true)
186 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
187 switch event.Key() {
188 case tcell.KeyEscape:
48ecca21 189 c.UI.Pages.RemovePage("serverMsgModal" + now)
3d2bd095
JH
190 }
191 return event
192 })
193
194 centeredFlex := tview.NewFlex().
195 AddItem(nil, 0, 1, false).
196 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
197 AddItem(nil, 0, 1, false).
198 AddItem(msgBox, 0, 2, true).
199 AddItem(nil, 0, 1, false), 0, 2, true).
200 AddItem(nil, 0, 1, false)
201
48ecca21 202 c.UI.Pages.AddPage("serverMsgModal"+now, centeredFlex, true, true)
3d2bd095
JH
203 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
204
205 return res, err
43ecc0f4
JH
206}
207
9174dbe8 208func (c *Client) showErrMsg(msg string) {
48ecca21 209 t := time.Now().Format(time.RFC850)
9174dbe8
JH
210
211 title := "| Error |"
212
213 msgBox := tview.NewTextView().SetScrollable(true)
214 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkRed)
215 msgBox.SetTitle(title).SetBorder(true)
216 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
217 switch event.Key() {
218 case tcell.KeyEscape:
48ecca21 219 c.UI.Pages.RemovePage("serverMsgModal" + t)
9174dbe8
JH
220 }
221 return event
222 })
223
224 centeredFlex := tview.NewFlex().
225 AddItem(nil, 0, 1, false).
226 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
227 AddItem(nil, 0, 1, false).
228 AddItem(msgBox, 0, 2, true).
229 AddItem(nil, 0, 1, false), 0, 2, true).
230 AddItem(nil, 0, 1, false)
231
48ecca21 232 c.UI.Pages.AddPage("serverMsgModal"+t, centeredFlex, true, true)
9174dbe8
JH
233 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
234}
235
43ecc0f4 236func handleGetFileNameList(c *Client, t *Transaction) (res []Transaction, err error) {
9174dbe8 237 if t.IsError() {
d005ef04
JH
238 c.showErrMsg(string(t.GetField(FieldError).Data))
239 c.Logger.Infof("Error: %s", t.GetField(FieldError).Data)
9174dbe8
JH
240 return res, err
241 }
242
43ecc0f4
JH
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]
d005ef04 257 f := NewField(FieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
43ecc0f4 258
d005ef04 259 if err := c.UI.HLClient.Send(*NewTransaction(TranGetFileNameList, nil, f)); err != nil {
43ecc0f4
JH
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))
d005ef04 271 f := NewField(FieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
43ecc0f4 272
d005ef04 273 if err := c.UI.HLClient.Send(*NewTransaction(TranGetFileNameList, nil, f)); err != nil {
43ecc0f4
JH
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 }
43ecc0f4
JH
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) {
d005ef04 326 newsText := string(t.GetField(FieldData).Data)
6988a057
JH
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{
d005ef04
JH
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,
6988a057
JH
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) {
d005ef04 385 exitUser := t.GetField(FieldUserID).Data
6988a057
JH
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
6988a057
JH
401func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
402 var users []User
403 for _, field := range t.Fields {
d005ef04
JH
404 // The Hotline protocol docs say that ClientGetUserNameList should only return FieldUsernameWithInfo (300)
405 // fields, but shxd sneaks in FieldChatSubject (115) so it's important to filter explicitly for the expected
71c56068
JH
406 // field type. Probably a good idea to do everywhere.
407 if bytes.Equal(field.ID, []byte{0x01, 0x2c}) {
408 u, err := ReadUser(field.Data)
409 if err != nil {
410 return res, err
411 }
412 users = append(users, *u)
413 }
6988a057
JH
414 }
415 c.UserList = users
416
417 c.renderUserList()
418
419 return res, err
420}
421
422func (c *Client) renderUserList() {
423 c.UI.userList.Clear()
424 for _, u := range c.UserList {
425 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
b1658a46 426 if flagBitmap.Bit(UserFlagAdmin) == 1 {
5dd57308 427 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 428 } else {
5dd57308 429 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 430 }
b198b22b 431 // TODO: fade if user is away
6988a057
JH
432 }
433}
434
435func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 436 if c.Pref.EnableBell {
89bbc565
JH
437 fmt.Println("\a")
438 }
439
d005ef04 440 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(FieldData).Data)
6988a057
JH
441
442 return res, err
443}
444
445func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 446 c.UserAccess = t.GetField(FieldUserAccess).Data
6988a057
JH
447
448 return res, err
449}
450
451func handleClientTranShowAgreement(c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 452 agreement := string(t.GetField(FieldData).Data)
6988a057
JH
453 agreement = strings.ReplaceAll(agreement, "\r", "\n")
454
72dd37f1 455 agreeModal := tview.NewModal().
6988a057
JH
456 SetText(agreement).
457 AddButtons([]string{"Agree", "Disagree"}).
458 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
459 if buttonIndex == 0 {
460 res = append(res,
461 *NewTransaction(
d005ef04
JH
462 TranAgreed, nil,
463 NewField(FieldUserName, []byte(c.Pref.Username)),
464 NewField(FieldUserIconID, c.Pref.IconBytes()),
465 NewField(FieldUserFlags, []byte{0x00, 0x00}),
466 NewField(FieldOptions, []byte{0x00, 0x00}),
6988a057
JH
467 ),
468 )
6988a057
JH
469 c.UI.Pages.HidePage("agreement")
470 c.UI.App.SetFocus(c.UI.chatInput)
471 } else {
f7e36225 472 _ = c.Disconnect()
6988a057
JH
473 c.UI.Pages.SwitchToPage("home")
474 }
475 },
476 )
477
72dd37f1 478 c.UI.Pages.AddPage("agreement", agreeModal, false, true)
b198b22b 479
6988a057
JH
480 return res, err
481}
482
483func handleClientTranLogin(c *Client, t *Transaction) (res []Transaction, err error) {
484 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
d005ef04 485 errMsg := string(t.GetField(FieldError).Data)
6988a057
JH
486 errModal := tview.NewModal()
487 errModal.SetText(errMsg)
488 errModal.AddButtons([]string{"Oh no"})
489 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
490 c.UI.Pages.RemovePage("errModal")
491 })
492 c.UI.Pages.RemovePage("joinServer")
493 c.UI.Pages.AddPage("errModal", errModal, false, true)
494
495 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
496
d005ef04
JH
497 c.Logger.Error(string(t.GetField(FieldError).Data))
498 return nil, errors.New("login error: " + string(t.GetField(FieldError).Data))
6988a057 499 }
40afb444 500 c.UI.Pages.AddAndSwitchToPage(serverUIPage, c.UI.renderServerUI(), true)
6988a057
JH
501 c.UI.App.SetFocus(c.UI.chatInput)
502
d005ef04 503 if err := c.Send(*NewTransaction(TranGetUserNameList, nil)); err != nil {
6988a057
JH
504 c.Logger.Errorw("err", "err", err)
505 }
506 return res, err
507}
508
509// JoinServer connects to a Hotline server and completes the login flow
d005ef04 510func (c *Client) Connect(address, login, passwd string) (err error) {
6988a057 511 // Establish TCP connection to server
d005ef04
JH
512 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
513 if err != nil {
6988a057
JH
514 return err
515 }
516
517 // Send handshake sequence
518 if err := c.Handshake(); err != nil {
519 return err
520 }
521
d005ef04 522 // Authenticate (send TranLogin 107)
6988a057
JH
523 if err := c.LogIn(login, passwd); err != nil {
524 return err
525 }
526
9d41bcdf
JH
527 // start keepalive go routine
528 go func() { _ = c.keepalive() }()
529
6988a057
JH
530 return nil
531}
532
f85c2d08
JH
533const keepaliveInterval = 300 * time.Second
534
9d41bcdf
JH
535func (c *Client) keepalive() error {
536 for {
f85c2d08 537 time.Sleep(keepaliveInterval)
d005ef04 538 _ = c.Send(*NewTransaction(TranKeepAlive, nil))
22c5767f 539 c.Logger.Debugw("Sent keepalive ping")
9d41bcdf
JH
540 }
541}
542
6988a057
JH
543var ClientHandshake = []byte{
544 0x54, 0x52, 0x54, 0x50, // TRTP
545 0x48, 0x4f, 0x54, 0x4c, // HOTL
546 0x00, 0x01,
547 0x00, 0x02,
548}
549
550var ServerHandshake = []byte{
551 0x54, 0x52, 0x54, 0x50, // TRTP
552 0x00, 0x00, 0x00, 0x00, // ErrorCode
553}
554
555func (c *Client) Handshake() error {
aebc4d36
JH
556 // Protocol ID 4 ‘TRTP’ 0x54 52 54 50
557 // Sub-protocol ID 4 User defined
558 // Version 2 1 Currently 1
559 // Sub-version 2 User defined
6988a057
JH
560 if _, err := c.Connection.Write(ClientHandshake); err != nil {
561 return fmt.Errorf("handshake write err: %s", err)
562 }
563
564 replyBuf := make([]byte, 8)
565 _, err := c.Connection.Read(replyBuf)
566 if err != nil {
567 return err
568 }
569
72dd37f1 570 if bytes.Equal(replyBuf, ServerHandshake) {
6988a057
JH
571 return nil
572 }
6988a057 573
b198b22b 574 // In the case of an error, client and server close the connection.
6988a057
JH
575 return fmt.Errorf("handshake response err: %s", err)
576}
577
578func (c *Client) LogIn(login string, password string) error {
579 return c.Send(
580 *NewTransaction(
d005ef04
JH
581 TranLogin, nil,
582 NewField(FieldUserName, []byte(c.Pref.Username)),
583 NewField(FieldUserIconID, c.Pref.IconBytes()),
584 NewField(FieldUserLogin, negateString([]byte(login))),
585 NewField(FieldUserPassword, negateString([]byte(password))),
6988a057
JH
586 ),
587 )
588}
589
6988a057
JH
590func (c *Client) Send(t Transaction) error {
591 requestNum := binary.BigEndian.Uint16(t.Type)
6988a057
JH
592
593 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
594 if t.IsReply == 0 {
902b8ac1 595 c.activeTasks[binary.BigEndian.Uint32(t.ID)] = &t
6988a057
JH
596 }
597
72dd37f1
JH
598 b, err := t.MarshalBinary()
599 if err != nil {
600 return err
601 }
902b8ac1
JH
602
603 var n int
72dd37f1 604 if n, err = c.Connection.Write(b); err != nil {
6988a057
JH
605 return err
606 }
607 c.Logger.Debugw("Sent Transaction",
608 "IsReply", t.IsReply,
609 "type", requestNum,
610 "sentBytes", n,
611 )
612 return nil
613}
614
615func (c *Client) HandleTransaction(t *Transaction) error {
616 var origT Transaction
617 if t.IsReply == 1 {
618 requestID := binary.BigEndian.Uint32(t.ID)
619 origT = *c.activeTasks[requestID]
620 t.Type = origT.Type
621 }
622
902b8ac1 623 if handler, ok := c.Handlers[binary.BigEndian.Uint16(t.Type)]; ok {
5978b74f 624 outT, _ := handler(c, t)
6988a057 625 for _, t := range outT {
902b8ac1
JH
626 if err := c.Send(t); err != nil {
627 return err
628 }
6988a057
JH
629 }
630 } else {
d005ef04 631 c.Logger.Debugw(
6988a057 632 "Unimplemented transaction type received",
902b8ac1 633 "RequestID", t.Type,
6988a057
JH
634 "TransactionID", t.ID,
635 )
636 }
637
638 return nil
639}
640
6988a057 641func (c *Client) Disconnect() error {
00d1ef67 642 return c.Connection.Close()
6988a057 643}
d005ef04
JH
644
645func (c *Client) HandleTransactions() error {
646 // Create a new scanner for parsing incoming bytes into transaction tokens
647 scanner := bufio.NewScanner(c.Connection)
648 scanner.Split(transactionScanner)
649
650 // Scan for new transactions and handle them as they come in.
651 for scanner.Scan() {
652 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
653 // scanner re-uses the buffer for subsequent scans.
654 buf := make([]byte, len(scanner.Bytes()))
655 copy(buf, scanner.Bytes())
656
657 var t Transaction
658 _, err := t.Write(buf)
659 if err != nil {
660 break
661 }
662 if err := c.HandleTransaction(&t); err != nil {
663 c.Logger.Errorw("Error handling transaction", "err", err)
664 }
665 }
666
667 if scanner.Err() == nil {
668 return scanner.Err()
669 }
670 return nil
671}