]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client.go
Fix string negation bug
[rbdr/mobius] / hotline / client.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
4 "bytes"
5 "embed"
6 "encoding/binary"
7 "errors"
8 "fmt"
6988a057
JH
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"
6988a057
JH
14 "math/big"
15 "math/rand"
16 "net"
17 "os"
18 "strings"
19 "time"
20)
21
4f3c459c
JH
22const (
23 trackerListPage = "trackerList"
24)
6988a057 25
22c599ab 26//go:embed banners/*.txt
6988a057
JH
27var bannerDir embed.FS
28
29type Bookmark struct {
30 Name string `yaml:"Name"`
31 Addr string `yaml:"Addr"`
32 Login string `yaml:"Login"`
33 Password string `yaml:"Password"`
34}
35
36type ClientPrefs struct {
37 Username string `yaml:"Username"`
38 IconID int `yaml:"IconID"`
39 Bookmarks []Bookmark `yaml:"Bookmarks"`
4f3c459c 40 Tracker string `yaml:"Tracker"`
6988a057
JH
41}
42
f7e36225
JH
43func (cp *ClientPrefs) IconBytes() []byte {
44 iconBytes := make([]byte, 2)
45 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
46 return iconBytes
47}
48
6988a057
JH
49func 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
64type Client struct {
95753255 65 cfgPath string
6988a057
JH
66 DebugBuf *DebugBuffer
67 Connection net.Conn
6988a057
JH
68 Login *[]byte
69 Password *[]byte
6988a057
JH
70 Flags *[]byte
71 ID *[]byte
72 Version []byte
73 UserAccess []byte
43ecc0f4 74 filePath []string
6988a057
JH
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
b198b22b
JH
89func 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,
6988a057 95 }
b198b22b 96 c.UI = NewUI(c)
6988a057 97
b198b22b 98 prefs, err := readConfig(cfgPath)
6988a057 99 if err != nil {
43ecc0f4
JH
100 fmt.Printf("unable to read config file %s", cfgPath)
101 os.Exit(1)
6988a057 102 }
b198b22b 103 c.pref = prefs
6988a057 104
b198b22b 105 return c
6988a057
JH
106}
107
6988a057
JH
108// DebugBuffer wraps a *tview.TextView and adds a Sync() method to make it available as a Zap logger
109type DebugBuffer struct {
110 TextView *tview.TextView
111}
112
113func (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
118func (db *DebugBuffer) Sync() error {
119 return nil
120}
121
6988a057
JH
122func randomBanner() string {
123 rand.Seed(time.Now().UnixNano())
124
ce348eb8
JH
125 bannerFiles, _ := bannerDir.ReadDir("banners")
126 file, _ := bannerDir.ReadFile("banners/" + bannerFiles[rand.Intn(len(bannerFiles))].Name())
6988a057
JH
127
128 return fmt.Sprintf("\n\n\nWelcome to...\n\n[red::b]%s[-:-:-]\n\n", file)
129}
130
6988a057
JH
131type clientTransaction struct {
132 Name string
133 Handler func(*Client, *Transaction) ([]Transaction, error)
134}
135
136func (ch clientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
137 return ch.Handler(cc, t)
138}
139
140type clientTHandler interface {
141 Handle(*Client, *Transaction) ([]Transaction, error)
142}
143
144type mockClientHandler struct {
145 mock.Mock
146}
147
148func (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
153var 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 },
43ecc0f4
JH
187 tranGetFileNameList: clientTransaction{
188 Name: "tranGetFileNameList",
189 Handler: handleGetFileNameList,
190 },
3d2bd095
JH
191 tranServerMsg: clientTransaction{
192 Name: "tranServerMsg",
193 Handler: handleTranServerMsg,
194 },
195}
196
197func 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
43ecc0f4
JH
228}
229
230func 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
4c3b4c7f 291 node := tview.NewTreeNode(fmt.Sprintf(" %-40s %10v KB", fn.Name, size))
43ecc0f4
JH
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).
246ed3a1 304 AddItem(nil, 0, 1, false), 60, 1, true).
43ecc0f4
JH
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
6988a057
JH
311}
312
313func 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)
43ecc0f4
JH
326 //c.UI.Pages.SwitchToPage("news")
327 //c.UI.App.SetFocus(newsTextView)
6988a057
JH
328 c.UI.App.Draw()
329
330 return res, err
331}
332
333func 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
372func 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
389const readBuffSize = 1024000 // 1KB - TODO: what should this be?
390
391func (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
421func (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
437func handleClientGetUserNameList(c *Client, t *Transaction) (res []Transaction, err error) {
438 var users []User
439 for _, field := range t.Fields {
71c56068
JH
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 }
6988a057
JH
450 }
451 c.UserList = users
452
453 c.renderUserList()
454
455 return res, err
456}
457
458func (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 {
5dd57308 463 _, _ = fmt.Fprintf(c.UI.userList, "[red::b]%s[-:-:-]\n", u.Name)
6988a057 464 } else {
5dd57308 465 _, _ = fmt.Fprintf(c.UI.userList, "%s\n", u.Name)
6988a057 466 }
b198b22b 467 // TODO: fade if user is away
6988a057
JH
468 }
469}
470
471func handleClientChatMsg(c *Client, t *Transaction) (res []Transaction, err error) {
5dd57308 472 _, _ = fmt.Fprintf(c.UI.chatBox, "%s \n", t.GetField(fieldData).Data)
6988a057
JH
473
474 return res, err
475}
476
477func handleClientTranUserAccess(c *Client, t *Transaction) (res []Transaction, err error) {
478 c.UserAccess = t.GetField(fieldUserAccess).Data
479
480 return res, err
481}
482
483func 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)),
f7e36225 496 NewField(fieldUserIconID, c.pref.IconBytes()),
6988a057
JH
497 NewField(fieldUserFlags, []byte{0x00, 0x00}),
498 NewField(fieldOptions, []byte{0x00, 0x00}),
499 ),
500 )
6988a057
JH
501 c.UI.Pages.HidePage("agreement")
502 c.UI.App.SetFocus(c.UI.chatInput)
503 } else {
f7e36225 504 _ = c.Disconnect()
6988a057
JH
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)
6988a057 512 c.UI.Pages.ShowPage("agreement ")
6988a057 513 c.UI.App.Draw()
b198b22b 514
6988a057
JH
515 return res, err
516}
517
518func 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
545func (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
565func (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
574var ClientHandshake = []byte{
575 0x54, 0x52, 0x54, 0x50, // TRTP
576 0x48, 0x4f, 0x54, 0x4c, // HOTL
577 0x00, 0x01,
578 0x00, 0x02,
579}
580
581var ServerHandshake = []byte{
582 0x54, 0x52, 0x54, 0x50, // TRTP
583 0x00, 0x00, 0x00, 0x00, // ErrorCode
584}
585
586func (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
6988a057
JH
601 if bytes.Compare(replyBuf, ServerHandshake) == 0 {
602 return nil
603 }
6988a057 604
b198b22b 605 // In the case of an error, client and server close the connection.
6988a057
JH
606 return fmt.Errorf("handshake response err: %s", err)
607}
608
609func (c *Client) LogIn(login string, password string) error {
610 return c.Send(
611 *NewTransaction(
612 tranLogin, nil,
613 NewField(fieldUserName, []byte(c.pref.Username)),
f7e36225 614 NewField(fieldUserIconID, c.pref.IconBytes()),
b25c4a19
JH
615 NewField(fieldUserLogin, negateString([]byte(login))),
616 NewField(fieldUserPassword, negateString([]byte(password))),
6988a057
JH
617 NewField(fieldVersion, []byte{0, 2}),
618 ),
619 )
620}
621
6988a057
JH
622func (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
646func (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
676func (c *Client) Connected() bool {
6988a057
JH
677 // c.Agreed == true &&
678 if c.UserAccess != nil {
679 return true
680 }
681 return false
682}
683
684func (c *Client) Disconnect() error {
685 err := c.Connection.Close()
686 if err != nil {
687 return err
688 }
689 return nil
690}