]> git.r.bdr.sh - rbdr/mobius/blame_incremental - hotline/client_conn.go
Update README.md
[rbdr/mobius] / hotline / client_conn.go
... / ...
CommitLineData
1package hotline
2
3import (
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
16type 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
39func (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
45func (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
78func (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
87func (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
97func (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
112func (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
125func (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
135func (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
149var 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.
158func 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
168const userInfoTemplate = `Nickname: %s
169Name: %s
170Account: %s
171Address: %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
190func 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
202func (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}