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