]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
minor: v0.12.0
[rbdr/mobius] / hotline / client.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
d005ef04 4 "bufio"
6988a057 5 "bytes"
ea2027a3 6 "context"
6988a057
JH
7 "embed"
8 "encoding/binary"
9 "errors"
10 "fmt"
6988a057
JH
11 "github.com/gdamore/tcell/v2"
12 "github.com/rivo/tview"
0197c3f5 13 "gopkg.in/yaml.v3"
ea2027a3 14 "log/slog"
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
48ecca21 52func (cp *ClientPrefs) AddBookmark(name, addr, login, pass string) {
da1e0d79 53 cp.Bookmarks = append(cp.Bookmarks, Bookmark{Addr: addr, Login: login, Password: pass})
da1e0d79
JH
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 74 UserAccess []byte
43ecc0f4 75 filePath []string
6988a057 76 UserList []User
ea2027a3 77 Logger *slog.Logger
6988a057 78 activeTasks map[uint32]*Transaction
e005c191 79 serverName string
6988a057 80
d005ef04 81 Pref *ClientPrefs
6988a057 82
5978b74f 83 Handlers map[uint16]ClientHandler
6988a057
JH
84
85 UI *UI
86
72dd37f1 87 Inbox chan *Transaction
6988a057
JH
88}
89
ea2027a3 90type ClientHandler func(context.Context, *Client, *Transaction) ([]Transaction, error)
5978b74f
JH
91
92func (c *Client) HandleFunc(transactionID uint16, handler ClientHandler) {
93 c.Handlers[transactionID] = handler
94}
95
ea2027a3 96func NewClient(username string, logger *slog.Logger) *Client {
d005ef04
JH
97 c := &Client{
98 Logger: logger,
99 activeTasks: make(map[uint32]*Transaction),
5978b74f 100 Handlers: make(map[uint16]ClientHandler),
d005ef04
JH
101 }
102 c.Pref = &ClientPrefs{Username: username}
103
104 return c
105}
106
ea2027a3 107func NewUIClient(cfgPath string, logger *slog.Logger) *Client {
b198b22b
JH
108 c := &Client{
109 cfgPath: cfgPath,
110 Logger: logger,
111 activeTasks: make(map[uint32]*Transaction),
112 Handlers: clientHandlers,
6988a057 113 }
b198b22b 114 c.UI = NewUI(c)
6988a057 115
b198b22b 116 prefs, err := readConfig(cfgPath)
6988a057 117 if err != nil {
ea2027a3
JH
118 logger.Error(fmt.Sprintf("unable to read config file %s\n", cfgPath))
119 os.Exit(1)
6988a057 120 }
d005ef04 121 c.Pref = prefs
6988a057 122
b198b22b 123 return c
6988a057
JH
124}
125
6988a057
JH
126// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
127type DebugBuffer struct {
128 TextView *tview.TextView
129}
130
131func (db *DebugBuffer) Write(p []byte) (int, error) {
132 return db.TextView.Write(p)
133}
134
7cd900d6 135// Sync is a noop function that dataFile to satisfy the zapcore.WriteSyncer interface
6988a057
JH
136func (db *DebugBuffer) Sync() error {
137 return nil
138}
139
6988a057
JH
140func randomBanner() string {
141 rand.Seed(time.Now().UnixNano())
142
ce348eb8
JH
143 bannerFiles, _ := bannerDir.ReadDir("banners")
144 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
145
146 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
147}
148
d005ef04 149type ClientTransaction struct {
6988a057
JH
150 Name string
151 Handler func(*Client, *Transaction) ([]Transaction, error)
152}
153
d005ef04 154func (ch ClientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
6988a057
JH
155 return ch.Handler(cc, t)
156}
157
d005ef04 158type ClientTHandler interface {
6988a057
JH
159 Handle(*Client, *Transaction) ([]Transaction, error)
160}
161
5978b74f
JH
162var clientHandlers = map[uint16]ClientHandler{
163 TranChatMsg: handleClientChatMsg,
164 TranLogin: handleClientTranLogin,
165 TranShowAgreement: handleClientTranShowAgreement,
166 TranUserAccess: handleClientTranUserAccess,
167 TranGetUserNameList: handleClientGetUserNameList,
168 TranNotifyChangeUser: handleNotifyChangeUser,
169 TranNotifyDeleteUser: handleNotifyDeleteUser,
170 TranGetMsgs: handleGetMsgs,
171 TranGetFileNameList: handleGetFileNameList,
172 TranServerMsg: handleTranServerMsg,
ea2027a3 173 TranKeepAlive: func(ctx context.Context, client *Client, transaction *Transaction) (t []Transaction, err error) {
5978b74f 174 return t, err
9d41bcdf 175 },
3d2bd095
JH
176}
177
ea2027a3 178func handleTranServerMsg(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
48ecca21 179 now := time.Now().Format(time.RFC850)
3d2bd095 180
d005ef04 181 msg := strings.ReplaceAll(string(t.GetField(FieldData).Data), "\r", "\n")
48ecca21 182 msg += "\n\nAt " + now
d005ef04 183 title := fmt.Sprintf("| Private Message From: %s |", t.GetField(FieldUserName).Data)
3d2bd095
JH
184
185 msgBox := tview.NewTextView().SetScrollable(true)
186 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkSlateBlue)
187 msgBox.SetTitle(title).SetBorder(true)
188 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
189 switch event.Key() {
190 case tcell.KeyEscape:
48ecca21 191 c.UI.Pages.RemovePage("serverMsgModal" + now)
3d2bd095
JH
192 }
193 return event
194 })
195
196 centeredFlex := tview.NewFlex().
197 AddItem(nil, 0, 1, false).
198 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
199 AddItem(nil, 0, 1, false).
200 AddItem(msgBox, 0, 2, true).
201 AddItem(nil, 0, 1, false), 0, 2, true).
202 AddItem(nil, 0, 1, false)
203
48ecca21 204 c.UI.Pages.AddPage("serverMsgModal"+now, centeredFlex, true, true)
3d2bd095
JH
205 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
206
207 return res, err
43ecc0f4
JH
208}
209
9174dbe8 210func (c *Client) showErrMsg(msg string) {
48ecca21 211 t := time.Now().Format(time.RFC850)
9174dbe8
JH
212
213 title := "| Error |"
214
215 msgBox := tview.NewTextView().SetScrollable(true)
216 msgBox.SetText(msg).SetBackgroundColor(tcell.ColorDarkRed)
217 msgBox.SetTitle(title).SetBorder(true)
218 msgBox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
219 switch event.Key() {
220 case tcell.KeyEscape:
48ecca21 221 c.UI.Pages.RemovePage("serverMsgModal" + t)
9174dbe8
JH
222 }
223 return event
224 })
225
226 centeredFlex := tview.NewFlex().
227 AddItem(nil, 0, 1, false).
228 AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
229 AddItem(nil, 0, 1, false).
230 AddItem(msgBox, 0, 2, true).
231 AddItem(nil, 0, 1, false), 0, 2, true).
232 AddItem(nil, 0, 1, false)
233
48ecca21 234 c.UI.Pages.AddPage("serverMsgModal"+t, centeredFlex, true, true)
9174dbe8
JH
235 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
236}
237
ea2027a3 238func handleGetFileNameList(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
9174dbe8 239 if t.IsError() {
d005ef04 240 c.showErrMsg(string(t.GetField(FieldError).Data))
9174dbe8
JH
241 return res, err
242 }
243
43ecc0f4
JH
244 fTree := tview.NewTreeView().SetTopLevel(1)
245 root := tview.NewTreeNode("Root")
246 fTree.SetRoot(root).SetCurrentNode(root)
247 fTree.SetBorder(true).SetTitle("| Files |")
248 fTree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
249 switch event.Key() {
250 case tcell.KeyEscape:
251 c.UI.Pages.RemovePage("files")
252 c.filePath = []string{}
253 case tcell.KeyEnter:
254 selectedNode := fTree.GetCurrentNode()
255
256 if selectedNode.GetText() == "<- Back" {
257 c.filePath = c.filePath[:len(c.filePath)-1]
d005ef04 258 f := NewField(FieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
43ecc0f4 259
d005ef04 260 if err := c.UI.HLClient.Send(*NewTransaction(TranGetFileNameList, nil, f)); err != nil {
ea2027a3 261 c.UI.HLClient.Logger.Error("err", "err", err)
43ecc0f4
JH
262 }
263 return event
264 }
265
266 entry := selectedNode.GetReference().(*FileNameWithInfo)
267
72dd37f1 268 if bytes.Equal(entry.Type[:], []byte("fldr")) {
ea2027a3 269 c.Logger.Info("get new directory listing", "name", string(entry.name))
43ecc0f4 270
72dd37f1 271 c.filePath = append(c.filePath, string(entry.name))
d005ef04 272 f := NewField(FieldFilePath, EncodeFilePath(strings.Join(c.filePath, "/")))
43ecc0f4 273
d005ef04 274 if err := c.UI.HLClient.Send(*NewTransaction(TranGetFileNameList, nil, f)); err != nil {
ea2027a3 275 c.UI.HLClient.Logger.Error("err", "err", err)
43ecc0f4
JH
276 }
277 } else {
278 // TODO: initiate file download
ea2027a3 279 c.Logger.Info("download file", "name", string(entry.name))
43ecc0f4
JH
280 }
281 }
282
283 return event
284 })
285
286 if len(c.filePath) > 0 {
287 node := tview.NewTreeNode("<- Back")
288 root.AddChild(node)
289 }
290
43ecc0f4
JH
291 for _, f := range t.Fields {
292 var fn FileNameWithInfo
72dd37f1
JH
293 err = fn.UnmarshalBinary(f.Data)
294 if err != nil {
295 return nil, nil
296 }
43ecc0f4 297
72dd37f1
JH
298 if bytes.Equal(fn.Type[:], []byte("fldr")) {
299 node := tview.NewTreeNode(fmt.Sprintf("[blue::]📁 %s[-:-:-]", fn.name))
43ecc0f4
JH
300 node.SetReference(&fn)
301 root.AddChild(node)
302 } else {
72dd37f1 303 size := binary.BigEndian.Uint32(fn.FileSize[:]) / 1024
43ecc0f4 304
72dd37f1 305 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.name, size))
43ecc0f4
JH
306 node.SetReference(&fn)
307 root.AddChild(node)
308 }
43ecc0f4
JH
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
ea2027a3 326func handleGetMsgs(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 327 newsText := string(t.GetField(FieldData).Data)
6988a057
JH
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)
aebc4d36
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
ea2027a3 346func handleNotifyChangeUser(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
6988a057 347 newUser := User{
d005ef04
JH
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,
6988a057
JH
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 {
6988a057
JH
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
ea2027a3 384func handleNotifyDeleteUser(ctx context.Context, 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
ea2027a3 401func handleClientGetUserNameList(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
6988a057
JH
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
ea2027a3 435func handleClientChatMsg(ctx context.Context, 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
ea2027a3 445func handleClientTranUserAccess(ctx context.Context, 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
ea2027a3 451func handleClientTranShowAgreement(ctx context.Context, 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
ea2027a3 483func handleClientTranLogin(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
6988a057 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 {
ea2027a3 504 c.Logger.Error("err", "err", err)
6988a057
JH
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))
9d41bcdf
JH
539 }
540}
541
6988a057
JH
542var ClientHandshake = []byte{
543 0x54, 0x52, 0x54, 0x50, // TRTP
544 0x48, 0x4f, 0x54, 0x4c, // HOTL
545 0x00, 0x01,
546 0x00, 0x02,
547}
548
549var ServerHandshake = []byte{
550 0x54, 0x52, 0x54, 0x50, // TRTP
551 0x00, 0x00, 0x00, 0x00, // ErrorCode
552}
553
554func (c *Client) Handshake() error {
aebc4d36
JH
555 // Protocol ID 4 ‘TRTP’ 0x54 52 54 50
556 // Sub-protocol ID 4 User defined
557 // Version 2 1 Currently 1
558 // Sub-version 2 User defined
6988a057
JH
559 if _, err := c.Connection.Write(ClientHandshake); err != nil {
560 return fmt.Errorf("handshake write err: %s", err)
561 }
562
563 replyBuf := make([]byte, 8)
564 _, err := c.Connection.Read(replyBuf)
565 if err != nil {
566 return err
567 }
568
72dd37f1 569 if bytes.Equal(replyBuf, ServerHandshake) {
6988a057
JH
570 return nil
571 }
6988a057 572
b198b22b 573 // In the case of an error, client and server close the connection.
6988a057
JH
574 return fmt.Errorf("handshake response err: %s", err)
575}
576
577func (c *Client) LogIn(login string, password string) error {
578 return c.Send(
579 *NewTransaction(
d005ef04
JH
580 TranLogin, nil,
581 NewField(FieldUserName, []byte(c.Pref.Username)),
582 NewField(FieldUserIconID, c.Pref.IconBytes()),
76d0c1f6
JH
583 NewField(FieldUserLogin, encodeString([]byte(login))),
584 NewField(FieldUserPassword, encodeString([]byte(password))),
6988a057
JH
585 ),
586 )
587}
588
6988a057
JH
589func (c *Client) Send(t Transaction) error {
590 requestNum := binary.BigEndian.Uint16(t.Type)
6988a057
JH
591
592 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
593 if t.IsReply == 0 {
902b8ac1 594 c.activeTasks[binary.BigEndian.Uint32(t.ID)] = &t
6988a057
JH
595 }
596
72dd37f1
JH
597 b, err := t.MarshalBinary()
598 if err != nil {
599 return err
600 }
902b8ac1
JH
601
602 var n int
72dd37f1 603 if n, err = c.Connection.Write(b); err != nil {
6988a057
JH
604 return err
605 }
ea2027a3 606 c.Logger.Debug("Sent Transaction",
6988a057
JH
607 "IsReply", t.IsReply,
608 "type", requestNum,
609 "sentBytes", n,
610 )
611 return nil
612}
613
ea2027a3 614func (c *Client) HandleTransaction(ctx context.Context, t *Transaction) error {
6988a057
JH
615 var origT Transaction
616 if t.IsReply == 1 {
617 requestID := binary.BigEndian.Uint32(t.ID)
618 origT = *c.activeTasks[requestID]
619 t.Type = origT.Type
620 }
621
902b8ac1 622 if handler, ok := c.Handlers[binary.BigEndian.Uint16(t.Type)]; ok {
ea2027a3
JH
623 c.Logger.Debug(
624 "Received transaction",
625 "IsReply", t.IsReply,
626 "type", binary.BigEndian.Uint16(t.Type),
627 )
93103127
JH
628 outT, err := handler(ctx, c, t)
629 if err != nil {
630 c.Logger.Error("error handling transaction", "err", err)
631 }
6988a057 632 for _, t := range outT {
902b8ac1
JH
633 if err := c.Send(t); err != nil {
634 return err
635 }
6988a057
JH
636 }
637 } else {
ea2027a3
JH
638 c.Logger.Debug(
639 "Unimplemented transaction type",
640 "IsReply", t.IsReply,
641 "type", binary.BigEndian.Uint16(t.Type),
6988a057
JH
642 )
643 }
644
645 return nil
646}
647
6988a057 648func (c *Client) Disconnect() error {
00d1ef67 649 return c.Connection.Close()
6988a057 650}
d005ef04 651
ea2027a3
JH
652
653func (c *Client) HandleTransactions(ctx context.Context) error {
d005ef04
JH
654 // Create a new scanner for parsing incoming bytes into transaction tokens
655 scanner := bufio.NewScanner(c.Connection)
656 scanner.Split(transactionScanner)
657
658 // Scan for new transactions and handle them as they come in.
659 for scanner.Scan() {
660 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
661 // scanner re-uses the buffer for subsequent scans.
662 buf := make([]byte, len(scanner.Bytes()))
663 copy(buf, scanner.Bytes())
664
665 var t Transaction
666 _, err := t.Write(buf)
667 if err != nil {
668 break
669 }
ea2027a3
JH
670
671 if err := c.HandleTransaction(ctx, &t); err != nil {
672 c.Logger.Error("Error handling transaction", "err", err)
d005ef04
JH
673 }
674 }
675
676 if scanner.Err() == nil {
677 return scanner.Err()
678 }
679 return nil
680}