]> git.r.bdr.sh - rbdr/mobius/blame - hotline/client_conn.go
Refactor and cleanup
[rbdr/mobius] / hotline / client_conn.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
6988a057 4 "encoding/binary"
6988a057
JH
5 "golang.org/x/crypto/bcrypt"
6 "math/big"
7 "net"
8)
9
10type byClientID []*ClientConn
11
12func (s byClientID) Len() int {
13 return len(s)
14}
15
16func (s byClientID) Swap(i, j int) {
17 s[i], s[j] = s[j], s[i]
18}
19
20func (s byClientID) Less(i, j int) bool {
21 return s[i].uint16ID() < s[j].uint16ID()
22}
23
24// ClientConn represents a client connected to a Server
25type ClientConn struct {
26 Connection net.Conn
27 ID *[]byte
28 Icon *[]byte
29 Flags *[]byte
72dd37f1 30 UserName []byte
6988a057 31 Account *Account
61c272e1 32 IdleTime int
6988a057
JH
33 Server *Server
34 Version *[]byte
35 Idle bool
aebc4d36 36 AutoReply []byte
6988a057 37 Transfers map[int][]*FileTransfer
bd1ce113 38 Agreed bool
6988a057
JH
39}
40
41func (cc *ClientConn) sendAll(t int, fields ...Field) {
42 for _, c := range sortedClients(cc.Server.Clients) {
43 cc.Server.outbox <- *NewTransaction(t, c.ID, fields...)
44 }
45}
46
47func (cc *ClientConn) handleTransaction(transaction *Transaction) error {
48 requestNum := binary.BigEndian.Uint16(transaction.Type)
49 if handler, ok := TransactionHandlers[requestNum]; ok {
50 for _, reqField := range handler.RequiredFields {
51 field := transaction.GetField(reqField.ID)
52
53 // Validate that required field is present
54 if field.ID == nil {
aebc4d36 55 cc.Server.Logger.Errorw(
6988a057 56 "Missing required field",
72dd37f1 57 "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
6988a057
JH
58 )
59 return nil
60 }
61
62 if len(field.Data) < reqField.minLen {
63 cc.Server.Logger.Infow(
64 "Field does not meet minLen",
72dd37f1 65 "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name, "FieldID", reqField.ID,
6988a057
JH
66 )
67 return nil
68 }
69 }
70 if !authorize(cc.Account.Access, handler.Access) {
71 cc.Server.Logger.Infow(
72 "Unauthorized Action",
72dd37f1 73 "Account", cc.Account.Login, "UserName", string(cc.UserName), "RequestType", handler.Name,
6988a057
JH
74 )
75 cc.Server.outbox <- cc.NewErrReply(transaction, handler.DenyMsg)
76
77 return nil
78 }
79
80 cc.Server.Logger.Infow(
81 "Received Transaction",
82 "login", cc.Account.Login,
72dd37f1 83 "name", string(cc.UserName),
6988a057
JH
84 "RequestType", handler.Name,
85 )
86
87 transactions, err := handler.Handler(cc, transaction)
88 if err != nil {
89 return err
90 }
91 for _, t := range transactions {
92 cc.Server.outbox <- t
93 }
94 } else {
95 cc.Server.Logger.Errorw(
96 "Unimplemented transaction type received",
72dd37f1 97 "UserName", string(cc.UserName), "RequestID", requestNum,
6988a057
JH
98 )
99 }
100
101 cc.Server.mux.Lock()
102 defer cc.Server.mux.Unlock()
103
61c272e1
JH
104 if requestNum != tranKeepAlive {
105 // reset the user idle timer
106 cc.IdleTime = 0
107
108 // if user was previously idle, mark as not idle and notify other connected clients that
109 // the user is no longer away
110 if cc.Idle {
111 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
112 flagBitmap.SetBit(flagBitmap, userFlagAway, 0)
113 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
114 cc.Idle = false
115
116 cc.sendAll(
117 tranNotifyChangeUser,
118 NewField(fieldUserID, *cc.ID),
119 NewField(fieldUserFlags, *cc.Flags),
120 NewField(fieldUserName, cc.UserName),
121 NewField(fieldUserIconID, *cc.Icon),
122 )
123 }
6988a057
JH
124 }
125
6988a057
JH
126 return nil
127}
128
129func (cc *ClientConn) Authenticate(login string, password []byte) bool {
130 if account, ok := cc.Server.Accounts[login]; ok {
131 return bcrypt.CompareHashAndPassword([]byte(account.Password), password) == nil
132 }
133
134 return false
135}
136
137func (cc *ClientConn) uint16ID() uint16 {
138 id, _ := byteToInt(*cc.ID)
139 return uint16(id)
140}
141
142// Authorize checks if the user account has the specified permission
143func (cc *ClientConn) Authorize(access int) bool {
144 if access == 0 {
145 return true
146 }
147
148 accessBitmap := big.NewInt(int64(binary.BigEndian.Uint64(*cc.Account.Access)))
149
150 return accessBitmap.Bit(63-access) == 1
151}
152
153// Disconnect notifies other clients that a client has disconnected
154func (cc ClientConn) Disconnect() {
155 cc.Server.mux.Lock()
156 defer cc.Server.mux.Unlock()
157
158 delete(cc.Server.Clients, binary.BigEndian.Uint16(*cc.ID))
159
003a743e 160 cc.notifyOthers(*NewTransaction(tranNotifyDeleteUser, nil, NewField(fieldUserID, *cc.ID)))
6988a057
JH
161
162 if err := cc.Connection.Close(); err != nil {
163 cc.Server.Logger.Errorw("error closing client connection", "RemoteAddr", cc.Connection.RemoteAddr())
164 }
165}
166
003a743e
JH
167// notifyOthers sends transaction t to other clients connected to the server
168func (cc ClientConn) notifyOthers(t Transaction) {
6988a057 169 for _, c := range sortedClients(cc.Server.Clients) {
bd1ce113 170 if c.ID != cc.ID && c.Agreed {
6988a057
JH
171 t.clientID = c.ID
172 cc.Server.outbox <- t
173 }
174 }
175}
176
6988a057
JH
177// NewReply returns a reply Transaction with fields for the ClientConn
178func (cc *ClientConn) NewReply(t *Transaction, fields ...Field) Transaction {
179 reply := Transaction{
180 Flags: 0x00,
181 IsReply: 0x01,
182 Type: t.Type,
183 ID: t.ID,
184 clientID: cc.ID,
185 ErrorCode: []byte{0, 0, 0, 0},
186 Fields: fields,
187 }
188
189 return reply
190}
191
192// NewErrReply returns an error reply Transaction with errMsg
193func (cc *ClientConn) NewErrReply(t *Transaction, errMsg string) Transaction {
194 return Transaction{
195 clientID: cc.ID,
196 Flags: 0x00,
197 IsReply: 0x01,
198 Type: []byte{0, 0},
199 ID: t.ID,
200 ErrorCode: []byte{0, 0, 0, 1},
201 Fields: []Field{
202 NewField(fieldError, []byte(errMsg)),
203 },
204 }
205}