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