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