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