]> git.r.bdr.sh - rbdr/mobius/blob - hotline/client_conn.go
Appease linter
[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 "strings"
11 "sync"
12 )
13
14 var clientConnSortFunc = func(a, b *ClientConn) int {
15 return cmp.Compare(
16 binary.BigEndian.Uint16(a.ID[:]),
17 binary.BigEndian.Uint16(b.ID[:]),
18 )
19 }
20
21 // ClientConn represents a client connected to a Server
22 type ClientConn struct {
23 Connection io.ReadWriteCloser
24 RemoteAddr string
25 ID ClientID
26 Icon []byte // TODO: make fixed size of 2
27 Version []byte // TODO: make fixed size of 2
28
29 FlagsMU sync.Mutex // TODO: move into UserFlags struct
30 Flags UserFlags
31
32 UserName []byte
33 Account *Account
34 IdleTime int
35 Server *Server // TODO: consider adding methods to interact with server
36 AutoReply []byte
37
38 ClientFileTransferMgr ClientFileTransferMgr
39
40 Logger *slog.Logger
41
42 mu sync.RWMutex
43 }
44
45 type ClientFileTransferMgr struct {
46 transfers map[FileTransferType]map[FileTransferID]*FileTransfer
47
48 mu sync.RWMutex
49 }
50
51 func NewClientFileTransferMgr() ClientFileTransferMgr {
52 return ClientFileTransferMgr{
53 transfers: map[FileTransferType]map[FileTransferID]*FileTransfer{
54 FileDownload: {},
55 FileUpload: {},
56 FolderDownload: {},
57 FolderUpload: {},
58 BannerDownload: {},
59 },
60 }
61 }
62
63 func (cftm *ClientFileTransferMgr) Add(ftType FileTransferType, ft *FileTransfer) {
64 cftm.mu.Lock()
65 defer cftm.mu.Unlock()
66
67 cftm.transfers[ftType][ft.RefNum] = ft
68 }
69
70 func (cftm *ClientFileTransferMgr) Get(ftType FileTransferType) []FileTransfer {
71 cftm.mu.Lock()
72 defer cftm.mu.Unlock()
73
74 fts := cftm.transfers[ftType]
75
76 var transfers []FileTransfer
77 for _, ft := range fts {
78 transfers = append(transfers, *ft)
79 }
80
81 return transfers
82 }
83
84 func (cftm *ClientFileTransferMgr) Delete(ftType FileTransferType, id FileTransferID) {
85 cftm.mu.Lock()
86 defer cftm.mu.Unlock()
87
88 delete(cftm.transfers[ftType], id)
89 }
90
91 func (cc *ClientConn) SendAll(t [2]byte, fields ...Field) {
92 for _, c := range cc.Server.ClientMgr.List() {
93 cc.Server.outbox <- NewTransaction(t, c.ID, fields...)
94 }
95 }
96
97 func (cc *ClientConn) handleTransaction(transaction Transaction) {
98 if handler, ok := cc.Server.handlers[transaction.Type]; ok {
99 if transaction.Type != TranKeepAlive {
100 cc.Logger.Info(tranTypeNames[transaction.Type])
101 }
102
103 for _, t := range handler(cc, &transaction) {
104 cc.Server.outbox <- t
105 }
106 }
107
108 if transaction.Type != TranKeepAlive {
109 cc.mu.Lock()
110 defer cc.mu.Unlock()
111
112 // reset the user idle timer
113 cc.IdleTime = 0
114
115 // if user was previously idle, mark as not idle and notify other connected clients that
116 // the user is no longer away
117 if cc.Flags.IsSet(UserFlagAway) {
118 cc.Flags.Set(UserFlagAway, 0)
119
120 cc.SendAll(
121 TranNotifyChangeUser,
122 NewField(FieldUserID, cc.ID[:]),
123 NewField(FieldUserFlags, cc.Flags[:]),
124 NewField(FieldUserName, cc.UserName),
125 NewField(FieldUserIconID, cc.Icon),
126 )
127 }
128 }
129 }
130
131 func (cc *ClientConn) Authenticate(login string, password []byte) bool {
132 if account := cc.Server.AccountManager.Get(login); account != nil {
133 return bcrypt.CompareHashAndPassword([]byte(account.Password), password) == nil
134 }
135
136 return false
137 }
138
139 // Authorize checks if the user account has the specified permission
140 func (cc *ClientConn) Authorize(access int) bool {
141 if cc.Account == nil {
142 return false
143 }
144 return cc.Account.Access.IsSet(access)
145 }
146
147 // Disconnect notifies other clients that a client has disconnected and closes the connection.
148 func (cc *ClientConn) Disconnect() {
149 cc.Server.ClientMgr.Delete(cc.ID)
150
151 for _, t := range cc.NotifyOthers(NewTransaction(TranNotifyDeleteUser, [2]byte{}, NewField(FieldUserID, cc.ID[:]))) {
152 cc.Server.outbox <- t
153 }
154
155 if err := cc.Connection.Close(); err != nil {
156 cc.Server.Logger.Debug("error closing client connection", "RemoteAddr", cc.RemoteAddr)
157 }
158 }
159
160 // NotifyOthers sends transaction t to other clients connected to the server
161 func (cc *ClientConn) NotifyOthers(t Transaction) (trans []Transaction) {
162 for _, c := range cc.Server.ClientMgr.List() {
163 if c.ID != cc.ID {
164 t.ClientID = c.ID
165 trans = append(trans, t)
166 }
167 }
168 return trans
169 }
170
171 // NewReply returns a reply Transaction with fields for the ClientConn
172 func (cc *ClientConn) NewReply(t *Transaction, fields ...Field) Transaction {
173 return Transaction{
174 IsReply: 1,
175 ID: t.ID,
176 ClientID: cc.ID,
177 Fields: fields,
178 }
179 }
180
181 // NewErrReply returns an error reply Transaction with errMsg
182 func (cc *ClientConn) NewErrReply(t *Transaction, errMsg string) []Transaction {
183 return []Transaction{
184 {
185 ClientID: cc.ID,
186 IsReply: 1,
187 ID: t.ID,
188 ErrorCode: [4]byte{0, 0, 0, 1},
189 Fields: []Field{
190 NewField(FieldError, []byte(errMsg)),
191 },
192 },
193 }
194 }
195
196 const userInfoTemplate = `Nickname: %s
197 Name: %s
198 Account: %s
199 Address: %s
200
201 -------- File Downloads ---------
202
203 %s
204 ------- Folder Downloads --------
205
206 %s
207 --------- File Uploads ----------
208
209 %s
210 -------- Folder Uploads ---------
211
212 %s
213 ------- Waiting Downloads -------
214
215 %s
216 `
217
218 func formatDownloadList(fts []FileTransfer) (s string) {
219 if len(fts) == 0 {
220 return "None.\n"
221 }
222
223 for _, dl := range fts {
224 s += dl.String()
225 }
226
227 return s
228 }
229
230 func (cc *ClientConn) String() string {
231 template := fmt.Sprintf(
232 userInfoTemplate,
233 cc.UserName,
234 cc.Account.Name,
235 cc.Account.Login,
236 cc.RemoteAddr,
237 formatDownloadList(cc.ClientFileTransferMgr.Get(FileDownload)),
238 formatDownloadList(cc.ClientFileTransferMgr.Get(FolderDownload)),
239 formatDownloadList(cc.ClientFileTransferMgr.Get(FileUpload)),
240 formatDownloadList(cc.ClientFileTransferMgr.Get(FolderUpload)),
241 "None.\n",
242 )
243
244 return strings.ReplaceAll(template, "\n", "\r")
245 }