/
client_handler.go
573 lines (471 loc) · 15 KB
/
client_handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
package server
import (
"crypto/sha512"
"encoding/hex"
"fmt"
"github.com/mbpolan/openmcs/internal/game"
"github.com/mbpolan/openmcs/internal/logger"
"github.com/mbpolan/openmcs/internal/model"
"github.com/mbpolan/openmcs/internal/network"
"github.com/mbpolan/openmcs/internal/network/request"
"github.com/mbpolan/openmcs/internal/network/response"
"github.com/mbpolan/openmcs/internal/store"
"github.com/pkg/errors"
"io"
"net"
"strings"
"time"
)
// clientVersion is the client version that is supported by the server.
const clientVersion = 317
// clientState is an enumeration of the various states a player's connection may be in.
type clientState int
const (
initializing clientState = iota
loggingIn
active
failed
)
// ClientHandler is responsible for managing the state and communications for a single client.
type ClientHandler struct {
conn net.Conn
game *game.Game
reader *network.ProtocolReader
writer *network.ProtocolWriter
closeChan chan *ClientHandler
lastHeartbeat time.Time
player *model.Player
store *store.Store
sessionKey uint64
state clientState
}
// NewClientHandler returns a new handler for a client connection. When the handler terminates, it will write to the
// provided closeChan to indicate its work is complete.
func NewClientHandler(conn net.Conn, closeChan chan *ClientHandler, store *store.Store, game *game.Game, sessionKey uint64) *ClientHandler {
return &ClientHandler{
conn: conn,
game: game,
store: store,
reader: network.NewReader(conn),
writer: network.NewWriter(conn),
closeChan: closeChan,
state: initializing,
sessionKey: sessionKey,
}
}
// Handle processes request for the client connection.
func (c *ClientHandler) Handle() {
run := true
defer c.conn.Close()
// continually process request from the client until we reach either a graceful close or error state
for run {
var nextState clientState
var err error
switch c.state {
case initializing:
nextState, err = c.handleInitialization()
case loggingIn:
nextState, err = c.handleLogin()
case active:
nextState, err = c.handleLoop()
case failed:
run = false
}
if err != nil {
c.logDisconnectError(err)
c.state = failed
} else {
c.state = nextState
}
}
// indicate this client handler can be cleaned up
c.closeChan <- c
// if the player was added to the game world, remove them and save their persistent data
if c.player != nil {
// remove the player from the game world
c.game.RemovePlayer(c.player)
err := c.store.SavePlayer(c.player)
if err != nil {
logger.Errorf("failed to save player %d: %s", c.player.ID, err)
}
}
}
// logDisconnectError possibly logs information about the player disconnecting.
func (c *ClientHandler) logDisconnectError(err error) {
// if the underlying cause is an eof, don't log an error since that indicates the client disconnected from us
cause1 := errors.Unwrap(err)
cause2 := errors.Unwrap(cause1)
if cause1 == io.EOF || cause2 == io.EOF {
username := "(unknown)"
if c.player != nil && c.player.Username != "" {
username = c.player.Username
}
logger.Infof("disconnecting player: %s", username)
return
}
logger.Errorf("disconnecting player due to error: %s", err)
}
func (c *ClientHandler) handleInitialization() (clientState, error) {
header, err := c.reader.Peek()
if err != nil {
return failed, errors.Wrap(err, "failed to read init packet header")
}
// expect an init request first
if header != request.InitRequestHeader {
return failed, fmt.Errorf("unexpected init packet header: %2x", header)
}
// read the contents of the init request
var req request.InitRequest
err = req.Read(c.reader)
if err != nil {
return failed, errors.Wrap(err, "unexpected login packet contents")
}
// write some padding bytes (ignored by client)
padding := response.NewBlankResponse(8)
err = padding.Write(c.writer)
if err != nil {
return failed, errors.Wrap(err, "failed to send padding")
}
// accept the session
resp := response.NewAcceptedInitResponse(c.sessionKey)
err = resp.Write(c.writer)
if err != nil {
return failed, errors.Wrap(err, "failed to send init response")
}
return loggingIn, nil
}
func (c *ClientHandler) handleLogin() (clientState, error) {
header, err := c.reader.Peek()
if err != nil {
return failed, errors.Wrap(err, "failed to read login packet header")
}
// expect a login request (either a reconnect attempt or a new connection)
if header != request.ReconnectLoginRequestHeader && header != request.NewLoginRequestHeader {
return failed, fmt.Errorf("unexpected login packet header: %2x", header)
}
// read the contents of the login request
var req request.LoginRequest
err = req.Read(c.reader)
if err != nil {
return failed, errors.Wrap(err, "unexpected login request contents")
}
// validate if the client is supported by the server
if req.Version != clientVersion {
resp := response.NewFailedInitResponse(response.InitGameUpdated)
err := resp.Write(c.writer)
return failed, err
}
// load the player's data, if it exists
player, err := c.store.LoadPlayer(req.Username)
if err != nil {
// fall through
logger.Errorf("failed to load player %s: %s", req.Username, err)
player = nil
}
// does a player with that username even exist?
if player == nil {
resp := response.NewFailedInitResponse(response.InitInvalidUsername)
err := resp.Write(c.writer)
return failed, err
}
// hash their password for comparison
passwordHash := c.hashPassword(req.Password)
if player.PasswordHash != passwordHash {
// TODO: track this as a failed login attempt
resp := response.NewFailedInitResponse(response.InitInvalidUsername)
err := resp.Write(c.writer)
return failed, err
}
// check if the player can be added to the game
result := c.game.ValidatePlayer(player)
if result != game.ValidationResultSuccess {
var resp response.Response
switch result {
case game.ValidationResultAlreadyLoggedIn:
resp = response.NewFailedInitResponse(response.InitAccountLoggedIn)
case game.ValidationResultNoCapacity:
resp = response.NewFailedInitResponse(response.InitServerFull)
default:
break
}
if resp != nil {
err = resp.Write(c.writer)
}
return failed, err
}
// the player has now authenticated and can be added to the game
c.player = player
// send a confirmation to the client
resp := response.NewLoggedInInitResponse(c.player.Type, c.player.Flagged)
err = resp.Write(c.writer)
if err != nil {
return failed, errors.Wrap(err, "failed to send logged in response")
}
// add the player to the game world
c.game.AddPlayer(player, req.IsLowMemory, c.writer)
logger.Infof("connected new player: %s (%s)", c.player.Username, c.conn.RemoteAddr().String())
return active, nil
}
// hashPassword computes a hash of the player's password.
func (c *ClientHandler) hashPassword(password string) string {
// use a sha512/256 hash algorithm for passwords
hash := sha512.Sum512_256([]byte(password))
return strings.ToLower(hex.EncodeToString(hash[:]))
}
func (c *ClientHandler) handleLoop() (clientState, error) {
header, err := c.reader.Peek()
if err != nil {
return failed, errors.Wrap(err, "unexpected error while waiting for packet header")
}
// maintain current state
var nextState = c.state
switch header {
case request.KeepAliveRequestHeader:
// idle/keep-alive
var req request.KeepAliveRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.lastHeartbeat = time.Now()
case request.FocusRequestHeader:
// client window focus has changed
var req request.FocusChangeRequest
err = req.Read(c.reader)
case request.ClientClickRequestHeader:
// the player clicked somewhere on the client window
var req request.ClientClickRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.MarkPlayerActive(c.player)
case request.RegionChangeRequestHeader:
// the player entered a new map region
var req request.RegionChangeRequest
err = req.Read(c.reader)
case request.CameraModeRequestHeader:
// the player moved their client's camera
var req request.CameraModeRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.MarkPlayerActive(c.player)
case request.RegionLoadedRequestHeader:
// the player's client finished loading a new map region
var req request.RegionLoadedRequest
err = req.Read(c.reader)
case request.RegionResetRequestHeader:
// the player's client loaded a certain number of map regions
var req request.RegionResetRequest
err = req.Read(c.reader)
case request.ReportRequestHeader:
// the player sent an abuse report
var req request.ReportRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.ProcessAbuseReport(c.player, req.Username, req.Reason, req.EnableMute)
case request.CloseInterfaceRequestHeader:
// the player's client dismissed the current interface, if any
var req request.CloseInterfaceRequest
err = req.Read(c.reader)
case request.PlayerIdleRequestHeader:
// the player has become idle
var req request.PlayerIdleRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.MarkPlayerInactive(c.player)
case request.PlayerChatRequestHeader:
// the player sent a chat message
var req request.PlayerChatRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoPlayerChat(c.player, req.Effect, req.Color, req.Text)
case request.ChatCommandRequestHeader:
// the player sent a chat command
var req request.ChatCommandRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoPlayerChatCommand(c.player, req.Text)
case request.PrivateChatRequestHeader:
// the player sent a private chat message
var req request.PrivateChatRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoPlayerPrivateChat(c.player, req.Recipient, req.Text)
case request.ChangeModesRequestHeader:
// the player changed one or more chat or interaction modes
var req request.ChangeModesRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.SetPlayerModes(c.player, req.PublicChat, req.PrivateChat, req.Interaction)
case request.WalkRequestHeader, request.WalkOnCommandRequestHeader, request.WalkMinimap:
// the player started walking to a destination on the map
var req request.WalkRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.WalkPlayer(c.player, req.Start, req.Waypoints)
case request.TakeGroundItemRequestHeader:
// the player tried to pick up a ground item
var req request.TakeGroundItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoTakeGroundItem(c.player, req.ItemID, req.GlobalPos)
case request.DropInventoryItemRequestHeader:
// the player dropped an inventory item
var req request.DropInventoryItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoDropInventoryItem(c.player, req.ItemID, req.InterfaceID, req.SecondaryActionID)
case request.SwapInventoryItemRequestHeader:
// the player rearranged an item in their inventory
var req request.SwapInventoryItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoSwapInventoryItem(c.player, req.FromSlot, req.ToSlot, req.InterfaceID)
case request.EquipItemRequestHeader:
// the player equipped an item from their inventory
var req request.EquipItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoEquipItem(c.player, req.ItemID, req.InterfaceID, req.SecondaryActionID)
case request.UnequipItemRequestHeader:
// the player unequipped an item from their equipment
var req request.UnequipItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoUnequipItem(c.player, req.ItemID, req.InterfaceID, req.SlotType)
case request.UseItemRequestHeader:
// the player initiated the default action on an item
var req request.UseItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoUseItem(c.player, req.ItemID, req.InterfaceID, req.ActionID)
case request.UseInventoryItemsRequestHeader:
// the player used an inventory item on another
var req request.UseInventoryItemsRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoUseInventoryItem(c.player, req.SourceItemID, req.SourceInterfaceID, req.SourceSlotID,
req.TargetItemID, req.TargetInterfaceID, req.TargetSlotID)
case request.AddFriendRequestHeader:
// the player requested another player be added to their friends list
var req request.ModifyFriendRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.AddFriend(c.player, req.Username)
case request.RemoveFriendRequestHeader:
// the player requested another player be removed from their friends list
var req request.ModifyFriendRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.RemoveFriend(c.player, req.Username)
case request.AddIgnoreRequestHeader:
// the player requested another player be added to their ignore list
var req request.ModifyIgnoreRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.AddIgnored(c.player, req.Username)
case request.RemoveIgnoreRequestHeader:
// the player requested another player be removed from their ignore list
var req request.ModifyIgnoreRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.RemoveIgnored(c.player, req.Username)
case request.InterfaceActionRequestHeader:
// the player has performed an action on an interface
var req request.InterfaceActionRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoInterfaceAction(c.player, req.Action)
case request.InteractObjectRequestHeader:
// the player interacted with an object
var req request.InteractObjectRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoInteractWithObject(c.player, req.Action, req.GlobalPos)
case request.CastSpellOnItemRequestHeader:
// the player cast a spell on an inventory item
var req request.CastSpellOnItemRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoCastSpellOnItem(c.player, req.SlotID, req.ItemID, req.InventoryInterfaceID, req.SpellInterfaceID)
case request.CharacterDesignRequestHeader:
// the player submitted a new character design
var req request.CharacterDesignRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoSetPlayerDesign(c.player, req.Gender, req.Base, req.BodyColors)
case request.AttackNPCRequestHeader:
// the player attacked an npc
var req request.AttackNPCRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoAttackNPC(c.player, req.TargetID)
case request.InteractWithNPCAction1RequestHeader,
request.InteractWithNPCAction2RequestHeader,
request.InteractWithNPCAction3RequestHeader,
request.InteractWithNPCAction4RequestHeader:
// the player interacted with an npc
var req request.InteractWithNPCRequest
err = req.Read(c.reader)
if err != nil {
break
}
c.game.DoInteractWithNPC(c.player, req.ActionIndex, req.TargetID)
default:
// unknown packet
err = fmt.Errorf("unexpected packet header: %2x", header)
}
if err != nil {
return failed, err
}
return nextState, nil
}