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