]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Replace zap logger with slog
[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
b129b7cb 293 _, err = fn.Write(f.Data)
72dd37f1
JH
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}) {
9cf66aea
JH
408 var user User
409 if _, err := user.Write(field.Data); err != nil {
410 return res, fmt.Errorf("unable to read user data: %w", err)
71c56068 411 }
9cf66aea
JH
412
413 users = append(users, user)
71c56068 414 }
6988a057
JH
415 }
416 c.UserList = users
417
418 c.renderUserList()
419
420 return res, err
421}
422
423func (c *Client) renderUserList() {
424 c.UI.userList.Clear()
425 for _, u := range c.UserList {
426 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(u.Flags)))
b1658a46 427 if flagBitmap.Bit(UserFlagAdmin) == 1 {
5dd57308 428 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 429 } else {
5dd57308 430 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 431 }
b198b22b 432 // TODO: fade if user is away
6988a057
JH
433 }
434}
435
ea2027a3 436func handleClientChatMsg(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 437 if c.Pref.EnableBell {
89bbc565
JH
438 fmt.Println("\a")
439 }
440
d005ef04 441 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(FieldData).Data)
6988a057
JH
442
443 return res, err
444}
445
ea2027a3 446func handleClientTranUserAccess(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 447 c.UserAccess = t.GetField(FieldUserAccess).Data
6988a057
JH
448
449 return res, err
450}
451
ea2027a3 452func handleClientTranShowAgreement(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
d005ef04 453 agreement := string(t.GetField(FieldData).Data)
6988a057
JH
454 agreement = strings.ReplaceAll(agreement, "\r", "\n")
455
72dd37f1 456 agreeModal := tview.NewModal().
6988a057
JH
457 SetText(agreement).
458 AddButtons([]string{"Agree", "Disagree"}).
459 SetDoneFunc(func(buttonIndex int, buttonLabel string) {
460 if buttonIndex == 0 {
461 res = append(res,
462 *NewTransaction(
d005ef04
JH
463 TranAgreed, nil,
464 NewField(FieldUserName, []byte(c.Pref.Username)),
465 NewField(FieldUserIconID, c.Pref.IconBytes()),
466 NewField(FieldUserFlags, []byte{0x00, 0x00}),
467 NewField(FieldOptions, []byte{0x00, 0x00}),
6988a057
JH
468 ),
469 )
6988a057
JH
470 c.UI.Pages.HidePage("agreement")
471 c.UI.App.SetFocus(c.UI.chatInput)
472 } else {
f7e36225 473 _ = c.Disconnect()
6988a057
JH
474 c.UI.Pages.SwitchToPage("home")
475 }
476 },
477 )
478
72dd37f1 479 c.UI.Pages.AddPage("agreement", agreeModal, false, true)
b198b22b 480
6988a057
JH
481 return res, err
482}
483
ea2027a3 484func handleClientTranLogin(ctx context.Context, c *Client, t *Transaction) (res []Transaction, err error) {
6988a057 485 if !bytes.Equal(t.ErrorCode, []byte{0, 0, 0, 0}) {
d005ef04 486 errMsg := string(t.GetField(FieldError).Data)
6988a057
JH
487 errModal := tview.NewModal()
488 errModal.SetText(errMsg)
489 errModal.AddButtons([]string{"Oh no"})
490 errModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
491 c.UI.Pages.RemovePage("errModal")
492 })
493 c.UI.Pages.RemovePage("joinServer")
494 c.UI.Pages.AddPage("errModal", errModal, false, true)
495
496 c.UI.App.Draw() // TODO: errModal doesn't render without this. wtf?
497
d005ef04
JH
498 c.Logger.Error(string(t.GetField(FieldError).Data))
499 return nil, errors.New("login error: " + string(t.GetField(FieldError).Data))
6988a057 500 }
40afb444 501 c.UI.Pages.AddAndSwitchToPage(serverUIPage, c.UI.renderServerUI(), true)
6988a057
JH
502 c.UI.App.SetFocus(c.UI.chatInput)
503
d005ef04 504 if err := c.Send(*NewTransaction(TranGetUserNameList, nil)); err != nil {
ea2027a3 505 c.Logger.Error("err", "err", err)
6988a057
JH
506 }
507 return res, err
508}
509
510// JoinServer connects to a Hotline server and completes the login flow
d005ef04 511func (c *Client) Connect(address, login, passwd string) (err error) {
6988a057 512 // Establish TCP connection to server
d005ef04
JH
513 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
514 if err != nil {
6988a057
JH
515 return err
516 }
517
518 // Send handshake sequence
519 if err := c.Handshake(); err != nil {
520 return err
521 }
522
d005ef04 523 // Authenticate (send TranLogin 107)
6988a057
JH
524 if err := c.LogIn(login, passwd); err != nil {
525 return err
526 }
527
9d41bcdf
JH
528 // start keepalive go routine
529 go func() { _ = c.keepalive() }()
530
6988a057
JH
531 return nil
532}
533
f85c2d08
JH
534const keepaliveInterval = 300 * time.Second
535
9d41bcdf
JH
536func (c *Client) keepalive() error {
537 for {
f85c2d08 538 time.Sleep(keepaliveInterval)
d005ef04 539 _ = c.Send(*NewTransaction(TranKeepAlive, nil))
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()),
76d0c1f6
JH
584 NewField(FieldUserLogin, encodeString([]byte(login))),
585 NewField(FieldUserPassword, encodeString([]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 }
ea2027a3 607 c.Logger.Debug("Sent Transaction",
6988a057
JH
608 "IsReply", t.IsReply,
609 "type", requestNum,
610 "sentBytes", n,
611 )
612 return nil
613}
614
ea2027a3 615func (c *Client) HandleTransaction(ctx context.Context, t *Transaction) error {
6988a057
JH
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 {
ea2027a3
JH
624 c.Logger.Debug(
625 "Received transaction",
626 "IsReply", t.IsReply,
627 "type", binary.BigEndian.Uint16(t.Type),
628 )
93103127
JH
629 outT, err := handler(ctx, c, t)
630 if err != nil {
631 c.Logger.Error("error handling transaction", "err", err)
632 }
6988a057 633 for _, t := range outT {
902b8ac1
JH
634 if err := c.Send(t); err != nil {
635 return err
636 }
6988a057
JH
637 }
638 } else {
ea2027a3
JH
639 c.Logger.Debug(
640 "Unimplemented transaction type",
641 "IsReply", t.IsReply,
642 "type", binary.BigEndian.Uint16(t.Type),
6988a057
JH
643 )
644 }
645
646 return nil
647}
648
6988a057 649func (c *Client) Disconnect() error {
00d1ef67 650 return c.Connection.Close()
6988a057 651}
d005ef04 652
ea2027a3 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}