]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client_conn.go
Limit chat message size to 8192 bytes
[rbdr/mobius] / hotline / client_conn.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
a2ef262a 4 "cmp"
6988a057 5 "encoding/binary"
df1ade54 6 "fmt"
6988a057 7 "golang.org/x/crypto/bcrypt"
d4c152a4 8 "io"
a6216dd8 9 "log/slog"
a2ef262a 10 "slices"
df1ade54
JH
11 "strings"
12 "sync"
6988a057
JH
13)
14
6988a057
JH
15// ClientConn represents a client connected to a Server
16type ClientConn struct {
d4c152a4
JH
17 Connection io.ReadWriteCloser
18 RemoteAddr string
a2ef262a 19 ID [2]byte
a7216f67 20 Icon []byte
a2ef262a
JH
21 flagsMU sync.Mutex
22 Flags UserFlags
72dd37f1 23 UserName []byte
6988a057 24 Account *Account
61c272e1 25 IdleTime int
6988a057 26 Server *Server
a7216f67 27 Version []byte
6988a057 28 Idle bool
aebc4d36 29 AutoReply []byte
df1ade54
JH
30
31 transfersMU sync.Mutex
32 transfers map[int]map[[4]byte]*FileTransfer
33
a6216dd8 34 logger *slog.Logger
a2ef262a
JH
35
36 sync.Mutex
6988a057
JH
37}
38
a2ef262a
JH
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...)
6988a057
JH
42 }
43}
44
a2ef262a
JH
45func (cc *ClientConn) handleTransaction(transaction Transaction) {
46 if handler, ok := TransactionHandlers[transaction.Type]; ok {
47 cc.logger.Debug("Received Transaction", "RequestType", transaction.Type)
6988a057 48
a2ef262a 49 for _, t := range handler(cc, &transaction) {
6988a057
JH
50 cc.Server.outbox <- t
51 }
6988a057
JH
52 }
53
54 cc.Server.mux.Lock()
55 defer cc.Server.mux.Unlock()
56
a2ef262a 57 if transaction.Type != TranKeepAlive {
61c272e1
JH
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 {
a2ef262a 64 cc.Flags.Set(UserFlagAway, 0)
61c272e1
JH
65 cc.Idle = false
66
67 cc.sendAll(
d005ef04 68 TranNotifyChangeUser,
a2ef262a
JH
69 NewField(FieldUserID, cc.ID[:]),
70 NewField(FieldUserFlags, cc.Flags[:]),
d005ef04
JH
71 NewField(FieldUserName, cc.UserName),
72 NewField(FieldUserIconID, cc.Icon),
61c272e1
JH
73 )
74 }
6988a057 75 }
6988a057
JH
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
6988a057
JH
86// Authorize checks if the user account has the specified permission
87func (cc *ClientConn) Authorize(access int) bool {
a2ef262a
JH
88 cc.Lock()
89 defer cc.Unlock()
90 if cc.Account == nil {
91 return false
92 }
187d6dc5 93 return cc.Account.Access.IsSet(access)
6988a057
JH
94}
95
96// Disconnect notifies other clients that a client has disconnected
0a92e50b 97func (cc *ClientConn) Disconnect() {
6988a057 98 cc.Server.mux.Lock()
a2ef262a
JH
99 delete(cc.Server.Clients, cc.ID)
100 cc.Server.mux.Unlock()
6988a057 101
a2ef262a 102 for _, t := range cc.notifyOthers(NewTransaction(TranNotifyDeleteUser, [2]byte{}, NewField(FieldUserID, cc.ID[:]))) {
21581958
JH
103 cc.Server.outbox <- t
104 }
6988a057
JH
105
106 if err := cc.Connection.Close(); err != nil {
a6216dd8 107 cc.Server.Logger.Error("error closing client connection", "RemoteAddr", cc.RemoteAddr)
6988a057
JH
108 }
109}
110
003a743e 111// notifyOthers sends transaction t to other clients connected to the server
21581958 112func (cc *ClientConn) notifyOthers(t Transaction) (trans []Transaction) {
a2ef262a
JH
113 cc.Server.mux.Lock()
114 defer cc.Server.mux.Unlock()
115 for _, c := range cc.Server.Clients {
5853654f 116 if c.ID != cc.ID {
6988a057 117 t.clientID = c.ID
21581958 118 trans = append(trans, t)
6988a057
JH
119 }
120 }
21581958 121 return trans
6988a057
JH
122}
123
6988a057
JH
124// NewReply returns a reply Transaction with fields for the ClientConn
125func (cc *ClientConn) NewReply(t *Transaction, fields ...Field) Transaction {
95159e55 126 return Transaction{
a2ef262a
JH
127 IsReply: 1,
128 ID: t.ID,
129 clientID: cc.ID,
130 Fields: fields,
6988a057 131 }
6988a057
JH
132}
133
134// NewErrReply returns an error reply Transaction with errMsg
a2ef262a
JH
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 },
6988a057
JH
145 },
146 }
147}
7cd900d6 148
a2ef262a
JH
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
7cd900d6
JH
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.
a2ef262a 158func sortedClients(unsortedClients map[[2]byte]*ClientConn) (clients []*ClientConn) {
7cd900d6
JH
159 for _, c := range unsortedClients {
160 clients = append(clients, c)
161 }
a2ef262a
JH
162
163 slices.SortFunc(clients, clientSortFunc)
164
7cd900d6
JH
165 return clients
166}
df1ade54
JH
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
c8bfd606 218 return strings.ReplaceAll(template, "\n", "\r")
df1ade54 219}