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