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