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