]> git.r.bdr.sh - rbdr/mobius/blob - hotline/client_conn.go
e527eba99db5ef1ad101abda698991572a622243
[rbdr/mobius] / hotline / client_conn.go
1 package hotline
2
3 import (
4 "cmp"
5 "encoding/binary"
6 "fmt"
7 "golang.org/x/crypto/bcrypt"
8 "io"
9 "log/slog"
10 "slices"
11 "strings"
12 "sync"
13 )
14
15 // ClientConn represents a client connected to a Server
16 type ClientConn struct {
17 Connection io.ReadWriteCloser
18 RemoteAddr string
19 ID [2]byte
20 Icon []byte
21 flagsMU sync.Mutex
22 Flags UserFlags
23 UserName []byte
24 Account *Account
25 IdleTime int
26 Server *Server
27 Version []byte
28 Idle bool
29 AutoReply []byte
30
31 transfersMU sync.Mutex
32 transfers map[int]map[[4]byte]*FileTransfer
33
34 logger *slog.Logger
35
36 sync.Mutex
37 }
38
39 func (cc *ClientConn) sendAll(t [2]byte, fields ...Field) {
40 for _, c := range cc.Server.Clients {
41 cc.Server.outbox <- NewTransaction(t, c.ID, fields...)
42 }
43 }
44
45 func (cc *ClientConn) handleTransaction(transaction Transaction) {
46 if handler, ok := TransactionHandlers[transaction.Type]; ok {
47 cc.logger.Debug("Received Transaction", "RequestType", transaction.Type)
48
49 for _, t := range handler(cc, &transaction) {
50 cc.Server.outbox <- t
51 }
52 }
53
54 cc.Server.mux.Lock()
55 defer cc.Server.mux.Unlock()
56
57 if transaction.Type != TranKeepAlive {
58 // reset the user idle timer
59 cc.IdleTime = 0
60
61 // if user was previously idle, mark as not idle and notify other connected clients that
62 // the user is no longer away
63 if cc.Idle {
64 cc.Flags.Set(UserFlagAway, 0)
65 cc.Idle = false
66
67 cc.sendAll(
68 TranNotifyChangeUser,
69 NewField(FieldUserID, cc.ID[:]),
70 NewField(FieldUserFlags, cc.Flags[:]),
71 NewField(FieldUserName, cc.UserName),
72 NewField(FieldUserIconID, cc.Icon),
73 )
74 }
75 }
76 }
77
78 func (cc *ClientConn) Authenticate(login string, password []byte) bool {
79 if account, ok := cc.Server.Accounts[login]; ok {
80 return bcrypt.CompareHashAndPassword([]byte(account.Password), password) == nil
81 }
82
83 return false
84 }
85
86 // Authorize checks if the user account has the specified permission
87 func (cc *ClientConn) Authorize(access int) bool {
88 cc.Lock()
89 defer cc.Unlock()
90 if cc.Account == nil {
91 return false
92 }
93 return cc.Account.Access.IsSet(access)
94 }
95
96 // Disconnect notifies other clients that a client has disconnected
97 func (cc *ClientConn) Disconnect() {
98 cc.Server.mux.Lock()
99 delete(cc.Server.Clients, cc.ID)
100 cc.Server.mux.Unlock()
101
102 for _, t := range cc.notifyOthers(NewTransaction(TranNotifyDeleteUser, [2]byte{}, NewField(FieldUserID, cc.ID[:]))) {
103 cc.Server.outbox <- t
104 }
105
106 if err := cc.Connection.Close(); err != nil {
107 cc.Server.Logger.Error("error closing client connection", "RemoteAddr", cc.RemoteAddr)
108 }
109 }
110
111 // notifyOthers sends transaction t to other clients connected to the server
112 func (cc *ClientConn) notifyOthers(t Transaction) (trans []Transaction) {
113 cc.Server.mux.Lock()
114 defer cc.Server.mux.Unlock()
115 for _, c := range cc.Server.Clients {
116 if c.ID != cc.ID {
117 t.clientID = c.ID
118 trans = append(trans, t)
119 }
120 }
121 return trans
122 }
123
124 // NewReply returns a reply Transaction with fields for the ClientConn
125 func (cc *ClientConn) NewReply(t *Transaction, fields ...Field) Transaction {
126 return Transaction{
127 IsReply: 1,
128 ID: t.ID,
129 clientID: cc.ID,
130 Fields: fields,
131 }
132 }
133
134 // NewErrReply returns an error reply Transaction with errMsg
135 func (cc *ClientConn) NewErrReply(t *Transaction, errMsg string) []Transaction {
136 return []Transaction{
137 {
138 clientID: cc.ID,
139 IsReply: 1,
140 ID: t.ID,
141 ErrorCode: [4]byte{0, 0, 0, 1},
142 Fields: []Field{
143 NewField(FieldError, []byte(errMsg)),
144 },
145 },
146 }
147 }
148
149 var clientSortFunc = func(a, b *ClientConn) int {
150 return cmp.Compare(
151 binary.BigEndian.Uint16(a.ID[:]),
152 binary.BigEndian.Uint16(b.ID[:]),
153 )
154 }
155
156 // sortedClients is a utility function that takes a map of *ClientConn and returns a sorted slice of the values.
157 // The purpose of this is to ensure that the ordering of client connections is deterministic so that test assertions work.
158 func sortedClients(unsortedClients map[[2]byte]*ClientConn) (clients []*ClientConn) {
159 for _, c := range unsortedClients {
160 clients = append(clients, c)
161 }
162
163 slices.SortFunc(clients, clientSortFunc)
164
165 return clients
166 }
167
168 const userInfoTemplate = `Nickname: %s
169 Name: %s
170 Account: %s
171 Address: %s
172
173 -------- File Downloads ---------
174
175 %s
176 ------- Folder Downloads --------
177
178 %s
179 --------- File Uploads ----------
180
181 %s
182 -------- Folder Uploads ---------
183
184 %s
185 ------- Waiting Downloads -------
186
187 %s
188 `
189
190 func formatDownloadList(fts map[[4]byte]*FileTransfer) (s string) {
191 if len(fts) == 0 {
192 return "None.\n"
193 }
194
195 for _, dl := range fts {
196 s += dl.String()
197 }
198
199 return s
200 }
201
202 func (cc *ClientConn) String() string {
203 cc.transfersMU.Lock()
204 defer cc.transfersMU.Unlock()
205 template := fmt.Sprintf(
206 userInfoTemplate,
207 cc.UserName,
208 cc.Account.Name,
209 cc.Account.Login,
210 cc.RemoteAddr,
211 formatDownloadList(cc.transfers[FileDownload]),
212 formatDownloadList(cc.transfers[FolderDownload]),
213 formatDownloadList(cc.transfers[FileUpload]),
214 formatDownloadList(cc.transfers[FolderUpload]),
215 "None.\n",
216 )
217
218 return strings.ReplaceAll(template, "\n", "\r")
219 }