]> git.r.bdr.sh - rbdr/mobius/blob - hotline/client.go
Create About This Area.txt
[rbdr/mobius] / hotline / client.go
1 package hotline
2
3 import (
4 "bufio"
5 "bytes"
6 "context"
7 "encoding/binary"
8 "fmt"
9 "io"
10 "log/slog"
11 "net"
12 "time"
13 )
14
15 type ClientPrefs struct {
16 Username string `yaml:"Username"`
17 IconID int `yaml:"IconID"`
18 Tracker string `yaml:"Tracker"`
19 EnableBell bool `yaml:"EnableBell"`
20 }
21
22 func (cp *ClientPrefs) IconBytes() []byte {
23 iconBytes := make([]byte, 2)
24 binary.BigEndian.PutUint16(iconBytes, uint16(cp.IconID))
25 return iconBytes
26 }
27
28 type Client struct {
29 Connection net.Conn
30 Logger *slog.Logger
31 Pref *ClientPrefs
32 Handlers map[[2]byte]ClientHandler
33 activeTasks map[[4]byte]*Transaction
34 UserList []User
35 }
36
37 type ClientHandler func(context.Context, *Client, *Transaction) ([]Transaction, error)
38
39 func (c *Client) HandleFunc(tranType [2]byte, handler ClientHandler) {
40 c.Handlers[tranType] = handler
41 }
42
43 func NewClient(username string, logger *slog.Logger) *Client {
44 c := &Client{
45 Logger: logger,
46 activeTasks: make(map[[4]byte]*Transaction),
47 Handlers: make(map[[2]byte]ClientHandler),
48 Pref: &ClientPrefs{Username: username},
49 }
50
51 return c
52 }
53
54 type ClientTransaction struct {
55 Name string
56 Handler func(*Client, *Transaction) ([]Transaction, error)
57 }
58
59 func (ch ClientTransaction) Handle(cc *Client, t *Transaction) ([]Transaction, error) {
60 return ch.Handler(cc, t)
61 }
62
63 type ClientTHandler interface {
64 Handle(*Client, *Transaction) ([]Transaction, error)
65 }
66
67 // JoinServer connects to a Hotline server and completes the login flow
68 func (c *Client) Connect(address, login, passwd string) (err error) {
69 // Establish TCP connection to server
70 c.Connection, err = net.DialTimeout("tcp", address, 5*time.Second)
71 if err != nil {
72 return err
73 }
74
75 // Send handshake sequence
76 if err := c.Handshake(); err != nil {
77 return err
78 }
79
80 // Authenticate (send TranLogin 107)
81
82 err = c.Send(
83 NewTransaction(
84 TranLogin, [2]byte{0, 0},
85 NewField(FieldUserName, []byte(c.Pref.Username)),
86 NewField(FieldUserIconID, c.Pref.IconBytes()),
87 NewField(FieldUserLogin, EncodeString([]byte(login))),
88 NewField(FieldUserPassword, EncodeString([]byte(passwd))),
89 ),
90 )
91 if err != nil {
92 return fmt.Errorf("error sending login transaction: %w", err)
93 }
94
95 // start keepalive go routine
96 go func() { _ = c.keepalive() }()
97
98 return nil
99 }
100
101 const keepaliveInterval = 300 * time.Second
102
103 func (c *Client) keepalive() error {
104 for {
105 time.Sleep(keepaliveInterval)
106 _ = c.Send(NewTransaction(TranKeepAlive, [2]byte{}))
107 }
108 }
109
110 var ClientHandshake = []byte{
111 0x54, 0x52, 0x54, 0x50, // TRTP
112 0x48, 0x4f, 0x54, 0x4c, // HOTL
113 0x00, 0x01,
114 0x00, 0x02,
115 }
116
117 var ServerHandshake = []byte{
118 0x54, 0x52, 0x54, 0x50, // TRTP
119 0x00, 0x00, 0x00, 0x00, // ErrorCode
120 }
121
122 func (c *Client) Handshake() error {
123 // Protocol Type 4 ‘TRTP’ 0x54 52 54 50
124 // Sub-protocol Type 4 User defined
125 // Version 2 1 Currently 1
126 // Sub-version 2 User defined
127 if _, err := c.Connection.Write(ClientHandshake); err != nil {
128 return fmt.Errorf("handshake write err: %s", err)
129 }
130
131 replyBuf := make([]byte, 8)
132 _, err := c.Connection.Read(replyBuf)
133 if err != nil {
134 return err
135 }
136
137 if bytes.Equal(replyBuf, ServerHandshake) {
138 return nil
139 }
140
141 // In the case of an error, client and server close the connection.
142 return fmt.Errorf("handshake response err: %s", err)
143 }
144
145 func (c *Client) Send(t Transaction) error {
146 requestNum := binary.BigEndian.Uint16(t.Type[:])
147
148 // if transaction is NOT reply, add it to the list to transactions we're expecting a response for
149 if t.IsReply == 0 {
150 c.activeTasks[t.ID] = &t
151 }
152
153 n, err := io.Copy(c.Connection, &t)
154 if err != nil {
155 return fmt.Errorf("error sending transaction: %w", err)
156 }
157
158 c.Logger.Debug("Sent Transaction",
159 "IsReply", t.IsReply,
160 "type", requestNum,
161 "sentBytes", n,
162 )
163 return nil
164 }
165
166 func (c *Client) HandleTransaction(ctx context.Context, t *Transaction) error {
167 var origT Transaction
168 if t.IsReply == 1 {
169 origT = *c.activeTasks[t.ID]
170 t.Type = origT.Type
171 }
172
173 if handler, ok := c.Handlers[t.Type]; ok {
174 c.Logger.Debug(
175 "Received transaction",
176 "IsReply", t.IsReply,
177 "type", t.Type[:],
178 )
179 outT, err := handler(ctx, c, t)
180 if err != nil {
181 c.Logger.Error("error handling transaction", "err", err)
182 }
183 for _, t := range outT {
184 if err := c.Send(t); err != nil {
185 return err
186 }
187 }
188 }
189
190 return nil
191 }
192
193 func (c *Client) Disconnect() error {
194 return c.Connection.Close()
195 }
196
197 func (c *Client) HandleTransactions(ctx context.Context) error {
198 // Create a new scanner for parsing incoming bytes into transaction tokens
199 scanner := bufio.NewScanner(c.Connection)
200 scanner.Split(transactionScanner)
201
202 // Scan for new transactions and handle them as they come in.
203 for scanner.Scan() {
204 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
205 // scanner re-uses the buffer for subsequent scans.
206 buf := make([]byte, len(scanner.Bytes()))
207 copy(buf, scanner.Bytes())
208
209 var t Transaction
210 _, err := t.Write(buf)
211 if err != nil {
212 break
213 }
214
215 if err := c.HandleTransaction(ctx, &t); err != nil {
216 c.Logger.Error("Error handling transaction", "err", err)
217 }
218 }
219
220 if scanner.Err() == nil {
221 return scanner.Err()
222 }
223 return nil
224 }