snakeia-server 1.1.3-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server.js ADDED
@@ -0,0 +1,1580 @@
1
+ /*
2
+ * Copyright (C) 2020 Eliastik (eliastiksofts.com)
3
+ *
4
+ * This file is part of "SnakeIA Server".
5
+ *
6
+ * "SnakeIA Server" is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * "SnakeIA Server" is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU General Public License
17
+ * along with "SnakeIA Server". If not, see <http://www.gnu.org/licenses/>.
18
+ */
19
+ const express = require("express");
20
+ const app = require("express")();
21
+ const fs = require("fs");
22
+ const http = require("http").createServer(app);
23
+ const io = require("socket.io")(http, {
24
+ cors: {
25
+ origin: true,
26
+ credentials: true
27
+ },
28
+ allowEIO3: true
29
+ });
30
+ const entities = require("html-entities");
31
+ const ejs = require("ejs");
32
+ const jwt = require("jsonwebtoken");
33
+ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
34
+ const cookieParser = require("cookie-parser");
35
+ const ioCookieParser = require("socket.io-cookie-parser");
36
+ const i18n = require("i18n");
37
+ const rateLimit = require("express-rate-limit");
38
+ const winston = require("winston");
39
+ const csrf = require("csurf");
40
+ const bodyParser = require("body-parser");
41
+ const node_config = require("config");
42
+
43
+ const games = {}; // Contains all the games processed by the server
44
+ process.env["ALLOW_CONFIG_MUTATIONS"] = true;
45
+ let config = node_config.get("ServerConfig"); // Server configuration (see default config file config.json)
46
+ const tokens = []; // User tokens
47
+ const invalidatedUserTokens = []; // Invalidated user tokens
48
+ const invalidatedAdminTokens = []; // Invalidated admin tokens
49
+
50
+ // Load config file
51
+ const configSources = node_config.util.getConfigSources();
52
+ const configFile = configSources[configSources.length - 1].name;
53
+
54
+ config.port = process.env.PORT || config.port;
55
+ const jsonWebTokenSecretKey = config.jsonWebTokenSecretKey && config.jsonWebTokenSecretKey.trim() != "" ? config.jsonWebTokenSecretKey : generateRandomJsonWebTokenSecretKey();
56
+ const jsonWebTokenSecretKeyAdmin = config.jsonWebTokenSecretKeyAdmin && config.jsonWebTokenSecretKeyAdmin.trim() != "" ? config.jsonWebTokenSecretKeyAdmin : generateRandomJsonWebTokenSecretKey(jsonWebTokenSecretKey);
57
+
58
+ // Update config to file
59
+ function updateConfigToFile() {
60
+ fs.writeFileSync(configFile, JSON.stringify({ "ServerConfig": config }, null, 4), "UTF-8");
61
+ config = node_config.get("ServerConfig");
62
+ }
63
+
64
+ // Logging
65
+ const logger = winston.createLogger({
66
+ level: config.logLevel,
67
+ format: winston.format.combine(
68
+ winston.format.timestamp(),
69
+ winston.format.simple(),
70
+ winston.format.errors({ stack: true })
71
+ ),
72
+ transports: config.enableLoggingFile ? [new winston.transports.File({ filename: config.logFile })] : [],
73
+ exceptionHandlers: config.enableLoggingFile ? [new winston.transports.File({ filename: config.errorLogFile })] : []
74
+ });
75
+
76
+ if(process.env.NODE_ENV !== "production") {
77
+ logger.add(new winston.transports.Console({
78
+ format: winston.format.colorize()
79
+ }));
80
+ }
81
+
82
+ if(config.proxyMode) {
83
+ app.enable("trust proxy");
84
+ }
85
+
86
+ // Internationalization
87
+ i18n.configure({
88
+ locales:["fr", "en"],
89
+ directory: __dirname + "/locales",
90
+ defaultLocale: "en",
91
+ queryParameter: "lang",
92
+ cookie: "lang"
93
+ });
94
+
95
+ // Game modules
96
+ const snakeia = require("snakeia");
97
+ const Snake = snakeia.Snake;
98
+ const Grid = snakeia.Grid;
99
+ const GameConstants = snakeia.GameConstants;
100
+ const GameEngine = config.enableMultithreading ? require("./GameEngineMultithreadingController")(logger) : snakeia.GameEngine;
101
+
102
+ class Player {
103
+ constructor(token, id, snake, ready, version) {
104
+ this.token = token;
105
+ this.id = id;
106
+ this.snake = snake;
107
+ this.ready = ready;
108
+ this.version = version;
109
+ }
110
+
111
+ get username() {
112
+ return Player.getUsername(this);
113
+ }
114
+
115
+ static getPlayer(array, id) {
116
+ for(let i = 0; i < array.length; i++) {
117
+ if(array[i] != null && array[i].id == id) {
118
+ return array[i];
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ static getPlayerAllGames(id) {
126
+ const keys = Object.keys(games);
127
+
128
+ for(let i = 0; i < keys.length; i++) {
129
+ const game = games[keys[i]];
130
+
131
+ if(game) {
132
+ const p = this.getPlayer(game.players, id);
133
+ const p2 = this.getPlayer(game.spectators, id);
134
+ if(p) return p;
135
+ if(p2) return p2;
136
+ }
137
+ }
138
+
139
+ return null;
140
+ }
141
+
142
+ static getPlayerToken(array, token) {
143
+ if(!token) return null;
144
+ for(let i = 0; i < array.length; i++) {
145
+ if(array[i] != null && array[i].token == token) {
146
+ return array[i];
147
+ }
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ static getPlayerAllGamesToken(token) {
154
+ if(!token) return null;
155
+ const keys = Object.keys(games);
156
+
157
+ for(let i = 0; i < keys.length; i++) {
158
+ const game = games[keys[i]];
159
+
160
+ if(game) {
161
+ const p = this.getPlayerToken(game.players, token);
162
+ const p2 = this.getPlayerToken(game.spectators, token);
163
+ if(p) return p;
164
+ if(p2) return p2;
165
+ }
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ static containsId(array, id) {
172
+ return Player.getPlayer(array, id) != null;
173
+ }
174
+
175
+ static containsToken(array, token) {
176
+ return Player.getPlayerToken(array, token) != null;
177
+ }
178
+
179
+ static containsIdAllGames(id) {
180
+ return Player.getPlayerAllGames(id) != null;
181
+ }
182
+
183
+ static containsTokenAllGames(token) {
184
+ return Player.getPlayerAllGamesToken(token) != null;
185
+ }
186
+
187
+ static getUsername(player) {
188
+ try {
189
+ const decoded_token = jwt.verify(player.token, jsonWebTokenSecretKey);
190
+ return decoded_token && decoded_token.username ? decoded_token.username : null;
191
+ } catch(e) {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ static getUsernameSocket(socket) {
197
+ try {
198
+ const decoded_token = jwt.verify(socket.handshake.auth.token || socket.handshake.query.token || socket.request.cookies.token, jsonWebTokenSecretKey);
199
+ return decoded_token && decoded_token.username ? decoded_token.username : null;
200
+ } catch(e) {
201
+ return null;
202
+ }
203
+ }
204
+ }
205
+
206
+ function getRoomsData() {
207
+ const rooms = [];
208
+ const keysRooms = Object.keys(games).filter(key => games[key] && !games[key]["private"]);
209
+
210
+ for(let i = 0; i < keysRooms.length; i++) {
211
+ const game = games[keysRooms[i]];
212
+
213
+ rooms.push({});
214
+ rooms[i]["borderWalls"] = false;
215
+ rooms[i]["generateWalls"] = false;
216
+ rooms[i]["players"] = Object.keys(game["players"]).length + game.numberAIToAdd;
217
+ rooms[i]["width"] = "???";
218
+ rooms[i]["height"] = "???";
219
+ rooms[i]["speed"] = game["game"].speed;
220
+ rooms[i]["code"] = keysRooms[i];
221
+ rooms[i]["maxPlayers"] = getMaxPlayers(keysRooms[i]);
222
+ rooms[i]["state"] = (game["started"] ? GameConstants.GameState.STARTED : game["timeoutPlay"] != null ? GameConstants.GameState.STARTING : game["searchingPlayers"] ? GameConstants.GameState.SEARCHING_PLAYERS : "");
223
+
224
+ if(game["game"].grid != null) {
225
+ rooms[i]["width"] = game["game"].grid.width;
226
+ rooms[i]["height"] = game["game"].grid.height;
227
+ rooms[i]["borderWalls"] = game["game"].grid.borderWalls;
228
+ rooms[i]["generateWalls"] = game["game"].grid.generateWalls;
229
+ }
230
+
231
+ if(game["game"].snake != null) {
232
+ rooms[i]["players"] = game["game"].snake.length;
233
+ }
234
+
235
+ if(game["spectators"] != null) {
236
+ rooms[i]["spectators"] = Object.keys(game["spectators"]).length;
237
+ }
238
+ }
239
+
240
+ return rooms;
241
+ }
242
+
243
+ function getRandomRoomKey() {
244
+ let r;
245
+
246
+ do {
247
+ r = Math.random().toString(36).substring(2, 10);
248
+ } while(r in games);
249
+
250
+ return r;
251
+ }
252
+
253
+ function generateRandomJsonWebTokenSecretKey(precValue) {
254
+ let key;
255
+
256
+ do {
257
+ key = require("crypto").randomBytes(256).toString("base64");
258
+ } while(precValue && precValue == key);
259
+
260
+ return key;
261
+ }
262
+
263
+ function getMaxPlayers(code) {
264
+ const game = games[code].game;
265
+
266
+ const heightGrid = parseInt(game.grid.height);
267
+ const widthGrid = parseInt(game.grid.width);
268
+
269
+ let numberEmptyCases = heightGrid * widthGrid;
270
+
271
+ if(game.grid.borderWalls) {
272
+ numberEmptyCases -= (((widthGrid + heightGrid) * 2) - 4);
273
+ }
274
+
275
+ if(game.grid.generateWalls) {
276
+ if(game.grid.borderWalls) {
277
+ numberEmptyCases -= ((heightGrid * widthGrid) * 0.1);
278
+ } else {
279
+ numberEmptyCases -= ((heightGrid * widthGrid) * 0.1675);
280
+ }
281
+ }
282
+
283
+ return Math.min(config.maxPlayers, Math.max(Math.round(numberEmptyCases / 5), 2));
284
+ }
285
+
286
+ function createRoom(data, socket) {
287
+ if(Object.keys(games).filter(key => games[key] != null).length < config.maxRooms && !Player.containsTokenAllGames(socket.handshake.auth.token || socket.handshake.query.token || socket.request.cookies.token) && !Player.containsIdAllGames(socket.id)) {
288
+ let heightGrid = 20;
289
+ let widthGrid = 20;
290
+ let borderWalls = false;
291
+ let generateWalls = false;
292
+ let speed = 8;
293
+ let enableAI = false;
294
+ let levelAI = null;
295
+ let validSettings = true;
296
+ let privateGame = false;
297
+
298
+ if(data.heightGrid == null || isNaN(data.heightGrid) || data.heightGrid < config.minGridSize || data.heightGrid > config.maxGridSize) {
299
+ validSettings = false;
300
+ } else {
301
+ heightGrid = data.heightGrid;
302
+ }
303
+
304
+ if(data.widthGrid == null || isNaN(data.widthGrid) || data.widthGrid < config.minGridSize || data.widthGrid > config.maxGridSize) {
305
+ validSettings = false;
306
+ } else {
307
+ widthGrid = data.widthGrid;
308
+ }
309
+
310
+ if(data.borderWalls == null) {
311
+ validSettings = false;
312
+ } else {
313
+ borderWalls = data.borderWalls ? true : false;
314
+ }
315
+
316
+ if(data.generateWalls == null) {
317
+ validSettings = false;
318
+ } else {
319
+ generateWalls = data.generateWalls ? true : false;
320
+ }
321
+
322
+ if(data.private == null) {
323
+ validSettings = false;
324
+ } else {
325
+ privateGame = data.private ? true : false;
326
+ }
327
+
328
+ if(data.speed == "custom") {
329
+ if(data.customSpeed == null || isNaN(data.customSpeed) || data.customSpeed < config.minSpeed || data.customSpeed > config.maxSpeed) {
330
+ validSettings = false;
331
+ } else {
332
+ speed = data.customSpeed;
333
+ }
334
+ } else if(data.speed == null || isNaN(data.speed) || data.speed < config.minSpeed || data.speed > config.maxSpeed) {
335
+ validSettings = false;
336
+ } else {
337
+ speed = data.speed;
338
+ }
339
+
340
+ if(data.enableAI && config.enableAI) {
341
+ enableAI = true;
342
+
343
+ if(data.levelAI && ["AI_LEVEL_RANDOM", "AI_LEVEL_LOW", "AI_LEVEL_DEFAULT", "AI_LEVEL_HIGH", "AI_LEVEL_ULTRA"].includes(data.levelAI)) {
344
+ levelAI = data.levelAI;
345
+ } else {
346
+ validSettings = false;
347
+ }
348
+ } else if(data.enableAI && !config.enableAI) {
349
+ validSettings = false;
350
+ }
351
+
352
+ if(validSettings) {
353
+ const code = getRandomRoomKey();
354
+ const grid = new Grid(widthGrid, heightGrid, generateWalls, borderWalls, false, null, false);
355
+ grid.reset();
356
+ grid.init();
357
+ const game = new GameEngine(grid, [], speed);
358
+
359
+ games[code] = {
360
+ game: game,
361
+ private: privateGame,
362
+ players: [],
363
+ spectators: [],
364
+ enableAI: enableAI,
365
+ levelAI: levelAI,
366
+ searchingPlayers: true,
367
+ started: false,
368
+ alreadyInit: false,
369
+ timeoutPlay: null,
370
+ timeStart: null,
371
+ timeoutMaxTimePlay: null
372
+ };
373
+
374
+ games[code].numberAIToAdd = enableAI ? Math.round(getMaxPlayers(code) / 2 - 1) : 0;
375
+
376
+ logger.info("room creation (code: " + code + ") - username: " + (Player.getUsernameSocket(socket)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id, {
377
+ "widthGrid": widthGrid,
378
+ "heightGrid": heightGrid,
379
+ "generateWalls": generateWalls,
380
+ "borderWalls": borderWalls,
381
+ "speed": speed,
382
+ "enableAI": enableAI,
383
+ "private": privateGame
384
+ });
385
+
386
+ if(socket != null) {
387
+ socket.emit("process", {
388
+ success: true,
389
+ code: code
390
+ });
391
+
392
+ setupRoom(code);
393
+ }
394
+ } else if(socket != null) {
395
+ socket.emit("process", {
396
+ success: false,
397
+ code: null,
398
+ errorCode: GameConstants.Error.INVALID_SETTINGS
399
+ });
400
+ }
401
+ } else if(socket != null) {
402
+ if(Player.containsTokenAllGames(socket.handshake.auth.token || socket.handshake.query.token || socket.request.cookies.token) || Player.containsIdAllGames(socket.id)) {
403
+ socket.emit("process", {
404
+ success: false,
405
+ code: null,
406
+ errorCode: "ALREADY_CREATED_ROOM"
407
+ });
408
+ } else {
409
+ socket.emit("process", {
410
+ success: false,
411
+ code: null,
412
+ errorCode: GameConstants.Error.MAX_ROOM_LIMIT_REACHED
413
+ });
414
+ }
415
+ }
416
+ }
417
+
418
+ function copySnakes(snakes) {
419
+ const copy = JSON.parse(JSON.stringify(snakes));
420
+
421
+ if(copy) {
422
+ copy.forEach(snake => {
423
+ delete snake["grid"];
424
+ if(snake.snakeAI && snake.snakeAI._aiLevelText) snake.snakeAI.aiLevelText = snake.snakeAI._aiLevelText;
425
+ });
426
+ }
427
+
428
+ return copy;
429
+ }
430
+
431
+ function setupRoom(code) {
432
+ const game = games[code].game;
433
+
434
+ game.onReset(() => {
435
+ io.to("room-" + code).emit("reset", {
436
+ "paused": game.paused,
437
+ "isReseted": game.isReseted,
438
+ "exited": game.exited,
439
+ "grid": game.grid,
440
+ "numFruit": game.numFruit,
441
+ "ticks": game.ticks,
442
+ "scoreMax": game.scoreMax,
443
+ "gameOver": game.gameOver,
444
+ "gameFinished": game.gameFinished,
445
+ "gameMazeWin": game.gameMazeWin,
446
+ "starting": game.starting,
447
+ "initialSpeed": game.initialSpeed,
448
+ "speed": game.speed,
449
+ "snakes": copySnakes(game.snakes),
450
+ "offsetFrame": game.speed * GameConstants.Setting.TIME_MULTIPLIER,
451
+ "confirmReset": false,
452
+ "confirmExit": false,
453
+ "getInfos": false,
454
+ "getInfosGame": false,
455
+ "errorOccurred": game.errorOccurred,
456
+ "timerToDisplay": config.enableMaxTimeGame ? (config.maxTimeGame - (Date.now() - game.timeStart)) / 1000 : -1,
457
+ "aiStuck": game.aiStuck,
458
+ "precAiStuck": false
459
+ });
460
+ });
461
+
462
+ game.onStart(() => {
463
+ io.to("room-" + code).emit("start", {
464
+ "snakes": copySnakes(game.snakes),
465
+ "grid": game.grid,
466
+ "starting": game.starting,
467
+ "countBeforePlay": game.countBeforePlay,
468
+ "paused": game.paused,
469
+ "isReseted": game.isReseted,
470
+ "confirmReset": false,
471
+ "confirmExit": false,
472
+ "getInfos": false,
473
+ "getInfosGame": false,
474
+ "errorOccurred": game.errorOccurred,
475
+ "searchingPlayers": false
476
+ });
477
+ });
478
+
479
+ game.onPause(() => {
480
+ io.to("room-" + code).emit("pause", {
481
+ "paused": game.paused,
482
+ "confirmReset": false,
483
+ "confirmExit": false,
484
+ "getInfos": false,
485
+ "getInfosGame": false,
486
+ "errorOccurred": game.errorOccurred
487
+ });
488
+ });
489
+
490
+ game.onContinue(() => {
491
+ io.to("room-" + code).emit("continue", {
492
+ "confirmReset": false,
493
+ "confirmExit": false,
494
+ "getInfos": false,
495
+ "getInfosGame": false,
496
+ "errorOccurred": game.errorOccurred
497
+ });
498
+ });
499
+
500
+ game.onStop(() => {
501
+ io.to("room-" + code).emit("stop", {
502
+ "paused": game.paused,
503
+ "scoreMax": game.scoreMax,
504
+ "gameOver": game.gameOver,
505
+ "gameFinished": game.gameFinished,
506
+ "confirmReset": false,
507
+ "confirmExit": false,
508
+ "getInfos": false,
509
+ "getInfosGame": false,
510
+ "errorOccurred": game.errorOccurred
511
+ });
512
+
513
+ if(games[code] != null) {
514
+ games[code].started = false;
515
+ games[code].searchingPlayers = true;
516
+ clearTimeout(games[code].timeoutMaxTimePlay);
517
+ }
518
+ });
519
+
520
+ game.onExit(() => {
521
+ io.to("room-" + code).emit("exit", {
522
+ "paused": game.paused,
523
+ "gameOver": game.gameOver,
524
+ "gameFinished": game.gameFinished,
525
+ "exited": game.exited,
526
+ "confirmReset": false,
527
+ "confirmExit": false,
528
+ "getInfos": false,
529
+ "getInfosGame": false,
530
+ "errorOccurred": game.errorOccurred
531
+ });
532
+ });
533
+
534
+ game.onKill(() => {
535
+ io.to("room-" + code).emit("kill", {
536
+ "paused": game.paused,
537
+ "gameOver": game.gameOver,
538
+ "killed": game.killed,
539
+ "snakes": copySnakes(game.snakes),
540
+ "gameFinished": game.gameFinished,
541
+ "grid": game.grid,
542
+ "confirmReset": false,
543
+ "confirmExit": false,
544
+ "getInfos": false,
545
+ "getInfosGame": false,
546
+ "errorOccurred": game.errorOccurred
547
+ });
548
+ });
549
+
550
+ game.onScoreIncreased(() => {
551
+ io.to("room-" + code).emit("scoreIncreased", {
552
+ "snakes": copySnakes(game.snakes),
553
+ "grid": game.grid,
554
+ "scoreMax": game.scoreMax,
555
+ "gameFinished": game.gameFinished,
556
+ "errorOccurred": game.errorOccurred
557
+ });
558
+ });
559
+
560
+ game.onUpdate(() => {
561
+ io.to("room-" + code).emit("update", {
562
+ "isReseted": game.isReseted,
563
+ "exited": game.exited,
564
+ "grid": game.grid,
565
+ "numFruit": game.numFruit,
566
+ "ticks": game.ticks,
567
+ "scoreMax": game.scoreMax,
568
+ "gameOver": game.gameOver,
569
+ "gameFinished": game.gameFinished,
570
+ "gameMazeWin": game.gameMazeWin,
571
+ "starting": game.starting,
572
+ "initialSpeed": game.initialSpeed,
573
+ "speed": game.speed,
574
+ "snakes": copySnakes(game.snakes),
575
+ "countBeforePlay": game.countBeforePlay,
576
+ "numFruit": game.numFruit,
577
+ "errorOccurred": game.errorOccurred,
578
+ "timerToDisplay": config.enableMaxTimeGame ? (config.maxTimeGame - (Date.now() - game.timeStart)) / 1000 : -1,
579
+ "aiStuck": game.aiStuck
580
+ });
581
+ });
582
+
583
+ game.onUpdateCounter(() => {
584
+ io.to("room-" + code).emit("updateCounter", {
585
+ "paused": game.paused,
586
+ "isReseted": game.isReseted,
587
+ "exited": game.exited,
588
+ "grid": game.grid,
589
+ "numFruit": game.numFruit,
590
+ "ticks": game.ticks,
591
+ "scoreMax": game.scoreMax,
592
+ "gameOver": game.gameOver,
593
+ "gameFinished": game.gameFinished,
594
+ "gameMazeWin": game.gameMazeWin,
595
+ "starting": game.starting,
596
+ "initialSpeed": game.initialSpeed,
597
+ "speed": game.speed,
598
+ "snakes": copySnakes(game.snakes),
599
+ "countBeforePlay": game.countBeforePlay,
600
+ "numFruit": game.numFruit,
601
+ "errorOccurred": game.errorOccurred,
602
+ "searchingPlayers": false,
603
+ "timerToDisplay": config.enableMaxTimeGame ? config.maxTimeGame / 1000 : -1
604
+ });
605
+ });
606
+ }
607
+
608
+ function cleanRooms() {
609
+ const keys = Object.keys(games);
610
+ const toRemove = [];
611
+
612
+ for(let i = 0; i < keys.length; i++) {
613
+ const game = games[keys[i]];
614
+
615
+ if(game != null) {
616
+ const players = Object.keys(game.players) + Object.keys(game.spectators);
617
+ const nb = players.length;
618
+
619
+ if(nb <= 0) {
620
+ toRemove.push(keys[i]);
621
+
622
+ if(game.game && game.game.kill) {
623
+ game.game.kill();
624
+ }
625
+ }
626
+ }
627
+ }
628
+
629
+ for(let i = 0; i < toRemove.length; i++) {
630
+ games[toRemove[i]] = null;
631
+ }
632
+ }
633
+
634
+ function gameMatchmaking(game, code) {
635
+ if(game != null && games[code] != null && games[code].searchingPlayers) {
636
+ let numberPlayers = game.players.length + games[code].numberAIToAdd;
637
+
638
+ if(numberPlayers < getMaxPlayers(code)) {
639
+ const toAdd = getMaxPlayers(code) - numberPlayers;
640
+
641
+ for(let i = 0; i < game.spectators.length && i < toAdd; i++) {
642
+ game.spectators[i].ready = true;
643
+ game.players.push(game.spectators[i]);
644
+ game.spectators[i] = null;
645
+ }
646
+
647
+ game.spectators = game.spectators.filter(spectator => spectator != null);
648
+ }
649
+
650
+ numberPlayers = game.players.length + games[code].numberAIToAdd;
651
+
652
+ if(numberPlayers - games[code].numberAIToAdd > 0) {
653
+ if(numberPlayers - games[code].numberAIToAdd > 1 && game.timeoutPlay == null) {
654
+ game.timeStart = Date.now() + config.playerWaitTime + 1000;
655
+
656
+ game.timeoutPlay = setTimeout(() => {
657
+ game.timeoutPlay = null;
658
+ startGame(code);
659
+ }, config.playerWaitTime);
660
+ } else if(numberPlayers - games[code].numberAIToAdd <= 1 && game.timeoutPlay != null) {
661
+ clearTimeout(game.timeoutPlay);
662
+ game.timeoutPlay = null;
663
+ game.timeStart = 0;
664
+ }
665
+
666
+ io.to("room-" + code).emit("init", {
667
+ "searchingPlayers": games[code].searchingPlayers,
668
+ "timeStart": game.timeStart != null ? game.timeStart - Date.now() : 0,
669
+ "playerNumber": numberPlayers,
670
+ "maxPlayers": getMaxPlayers(code),
671
+ "spectatorMode": false,
672
+ "errorOccurred": game.game.errorOccurred,
673
+ "onlineMaster": false,
674
+ "onlineMode": true,
675
+ "enableRetryPauseMenu": false,
676
+ "countBeforePlay": game.countBeforePlay,
677
+ "initialSpeed": game.game.initialSpeed,
678
+ "speed": game.game.speed,
679
+ });
680
+
681
+ io.to(game.players[0].id).emit("init", {
682
+ "onlineMaster": true
683
+ });
684
+ }
685
+ }
686
+
687
+ setupSpectators(code);
688
+ }
689
+
690
+ function startGame(code) {
691
+ const game = games[code];
692
+
693
+ if(game != null) {
694
+ if(game.timeoutPlay != null) {
695
+ clearTimeout(game.timeoutPlay);
696
+ game.timeoutPlay = null;
697
+ }
698
+
699
+ game.searchingPlayers = false;
700
+ game.started = true;
701
+ game.game.snakes = [];
702
+ game.game.grid.reset();
703
+ game.game.grid.init();
704
+
705
+ for(let i = 0; i < game.players.length; i++) {
706
+ const username = game.players[i].username;
707
+
708
+ game.players[i].snake = new Snake(null, null, game.game.grid, null, null, null, username);
709
+ game.game.snakes.push(game.players[i].snake);
710
+
711
+ io.to(game.players[i].id).emit("init", {
712
+ "currentPlayer": (i + 1),
713
+ "spectatorMode": false
714
+ });
715
+ }
716
+
717
+ if(game.enableAI) {
718
+ for(let i = 0; i < game.numberAIToAdd; i++) {
719
+ const snakeAI = new Snake(null, null, game.game.grid, GameConstants.PlayerType.AI, game.levelAI);
720
+ game.game.snakes.push(snakeAI);
721
+ }
722
+ }
723
+
724
+ if(config.enableMaxTimeGame) {
725
+ clearTimeout(game.timeoutMaxTimePlay);
726
+ game.game.timeStart = Date.now() + 5000;
727
+ game.timeoutMaxTimePlay = setTimeout(() => {
728
+ game.game.stop(true);
729
+ }, config.maxTimeGame + 5000);
730
+ }
731
+
732
+ if(!game.alreadyInit) {
733
+ game.game.init();
734
+ game.game.start();
735
+ game.alreadyInit = true;
736
+ } else {
737
+ game.game.countBeforePlay = 3;
738
+ game.game.init();
739
+ game.game.reset();
740
+ }
741
+
742
+ setupSpectators(code);
743
+ }
744
+ }
745
+
746
+ function setupSpectators(code) {
747
+ const game = games[code];
748
+
749
+ if(game != null) {
750
+ for(let i = 0; i < game.spectators.length; i++) {
751
+ io.to(game.spectators[i].id).emit("init", {
752
+ "spectatorMode": true,
753
+ "onlineMode": true,
754
+ "enableRetryPauseMenu": false
755
+ });
756
+ }
757
+ }
758
+ }
759
+
760
+ function sendStatus(code) {
761
+ if(config.enableMultithreading) {
762
+ const game = games[code];
763
+
764
+ if(game != null) {
765
+ for(let i = 0; i < game.players.length; i++) {
766
+ game.game.key(game.players[i].snake.lastKey, i + 1);
767
+ if(game.players[i].snake.gameOver) game.game.setGameOver(i + 1);
768
+ }
769
+ }
770
+ }
771
+ }
772
+
773
+ function exitGame(game, socket, code) {
774
+ if(game) {
775
+ logger.info("exit game (code: " + code + ") - username: " + Player.getUsernameSocket(socket) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
776
+
777
+ if(Player.containsId(game.players, socket.id) && Player.getPlayer(game.players, socket.id).snake != null) {
778
+ Player.getPlayer(game.players, socket.id).snake.gameOver = true;
779
+ sendStatus(code);
780
+ }
781
+
782
+ socket.emit("kill", {
783
+ "killed": true
784
+ });
785
+
786
+ socket.leave("room-" + code);
787
+
788
+ if(Player.containsId(game.players, socket.id)) {
789
+ game.players = game.players.filter(player => player.id != socket.id);
790
+ }
791
+
792
+ if(Player.containsId(game.spectators, socket.id)) {
793
+ game.spectators = game.spectators.filter(spectator => spectator.id != socket.id);
794
+ }
795
+
796
+ cleanRooms();
797
+ gameMatchmaking(game, code);
798
+ }
799
+ }
800
+
801
+ function ipBanned(ip) {
802
+ if(ip.substr(0, 7) == "::ffff:") {
803
+ ip = ip.substr(7, ip.length);
804
+ }
805
+
806
+ return new Promise((resolve, reject) => {
807
+ config.ipBan.forEach(ipBanned => {
808
+ if(ipBanned == ip) {
809
+ resolve();
810
+ }
811
+ });
812
+
813
+ reject();
814
+ });
815
+ }
816
+
817
+ function usernameBanned(username) {
818
+ return new Promise((resolve, reject) => {
819
+ config.usernameBan.forEach(usernameBanned => {
820
+ if(username.toLowerCase().indexOf(usernameBanned.toLowerCase()) > -1) {
821
+ resolve();
822
+ }
823
+ });
824
+
825
+ reject();
826
+ });
827
+ }
828
+
829
+ function usernameAlreadyInUse(username) {
830
+ return new Promise((resolve, reject) => {
831
+ tokens.forEach(token => {
832
+ try {
833
+ const otherUsername = jwt.verify(token, jsonWebTokenSecretKey).username;
834
+
835
+ if(otherUsername) {
836
+ if(otherUsername.toLowerCase().indexOf(username.toLowerCase()) > -1) {
837
+ resolve();
838
+ }
839
+ }
840
+ } catch(e) {}
841
+ });
842
+
843
+ reject();
844
+ });
845
+ }
846
+
847
+ function verifyRecaptcha(response) {
848
+ if(config.enableRecaptcha && config.recaptchaPrivateKey && config.recaptchaPrivateKey.trim() != "" && config.recaptchaPublicKey && config.recaptchaPublicKey.trim() != "") {
849
+ const params = new URLSearchParams();
850
+ params.append("secret", config.recaptchaPrivateKey);
851
+ params.append("response", response);
852
+
853
+ return new Promise((resolve, reject) => {
854
+ fetch(config.recaptchaApiUrl, {
855
+ method: "POST",
856
+ body: params
857
+ }).then(res => res.json()).then(json => {
858
+ if(json && json.success) {
859
+ resolve();
860
+ } else {
861
+ reject();
862
+ }
863
+ });
864
+ });
865
+ } else {
866
+ return new Promise((resolve, reject) => {
867
+ resolve();
868
+ });
869
+ }
870
+ }
871
+
872
+ function verifyFormAuthentication(body) {
873
+ return new Promise((resolve, reject) => {
874
+ verifyRecaptcha(body["g-recaptcha-response"]).then(() => {
875
+ const username = body["username"];
876
+
877
+ if(username && username.trim() != "" && username.length >= config.minCharactersUsername && username.length <= config.maxCharactersUsername) {
878
+ usernameBanned(username).then(() => {
879
+ reject("BANNED_USERNAME");
880
+ }, () => {
881
+ usernameAlreadyInUse(username).then(() => {
882
+ reject("USERNAME_ALREADY_IN_USE");
883
+ }, () => {
884
+ resolve();
885
+ });
886
+ });
887
+ } else {
888
+ reject("BAD_USERNAME");
889
+ }
890
+ }, () => {
891
+ reject("INVALID_RECAPTCHA");
892
+ });
893
+ });
894
+ }
895
+
896
+ app.engine("html", ejs.renderFile);
897
+ app.set("view engine", "html");
898
+ app.disable("view cache");
899
+
900
+ app.use(express.static("assets"));
901
+ app.use(express.json());
902
+ app.use(express.urlencoded({ extended: true }));
903
+ app.use(cookieParser());
904
+ app.use(i18n.init);
905
+
906
+ // Rate limiter
907
+ app.use("/authentication", rateLimit({
908
+ windowMs: config.authentWindowMs,
909
+ max: config.authentMaxRequest
910
+ }));
911
+
912
+ // IP ban
913
+ app.use(function(req, res, next) {
914
+ ipBanned(req.ip).then(() => {
915
+ res.render(__dirname + "/views/banned.html", {
916
+ contact: config.contactBan
917
+ });
918
+ res.end();
919
+ }, () => {
920
+ next()
921
+ });
922
+ });
923
+
924
+ app.get("/", function(req, res) {
925
+ res.render(__dirname + "/views/index.html", {
926
+ version: config.version,
927
+ engineVersion: GameConstants.Setting.APP_VERSION
928
+ });
929
+ });
930
+
931
+ app.get("/authentication", function(req, res) {
932
+ if(req.cookies && config.enableAuthentication) {
933
+ let err = false;
934
+
935
+ checkAuthenticationExpress(req).catch(() => err = true).finally(() => {
936
+ res.render(__dirname + "/views/authentication.html", {
937
+ publicKey: config.recaptchaPublicKey,
938
+ enableRecaptcha: config.enableRecaptcha,
939
+ errorRecaptcha: false,
940
+ errorUsername: false,
941
+ errorUsernameBanned: false,
942
+ errorUsernameAlreadyInUse: false,
943
+ success: false,
944
+ authent: !err,
945
+ locale: i18n.getLocale(req),
946
+ min: config.minCharactersUsername,
947
+ max: config.maxCharactersUsername,
948
+ enableMaxTimeGame: config.enableMaxTimeGame,
949
+ maxTimeGame: config.maxTimeGame
950
+ });
951
+ });
952
+ } else {
953
+ res.end();
954
+ }
955
+ });
956
+
957
+ app.post("/authentication", function(req, res) {
958
+ if(req.cookies && config.enableAuthentication) {
959
+ let err = false;
960
+
961
+ checkAuthenticationExpress(req).catch(() => err = true).finally(() => {
962
+ if(err) {
963
+ verifyFormAuthentication(req.body).then(() => {
964
+ const username = req.body["username"];
965
+ const id = req.query.id;
966
+
967
+ const token = jwt.sign({
968
+ username: username
969
+ }, jsonWebTokenSecretKey, { expiresIn: config.authenticationTime / 1000 });
970
+
971
+ res.cookie("token", token, { expires: new Date(Date.now() + config.authenticationTime), httpOnly: true, sameSite: "None", secure: (req.protocol == "https" ? true : false) });
972
+
973
+ res.render(__dirname + "/views/authentication.html", {
974
+ publicKey: config.recaptchaPublicKey,
975
+ enableRecaptcha: config.enableRecaptcha,
976
+ errorRecaptcha: false,
977
+ errorUsername: false,
978
+ errorUsernameBanned: false,
979
+ errorUsernameAlreadyInUse: false,
980
+ success: true,
981
+ authent: false,
982
+ locale: i18n.getLocale(req),
983
+ min: config.minCharactersUsername,
984
+ max: config.maxCharactersUsername,
985
+ enableMaxTimeGame: config.enableMaxTimeGame,
986
+ maxTimeGame: config.maxTimeGame
987
+ });
988
+
989
+ logger.info("authentication - username: " + username + " - ip: " + req.ip);
990
+
991
+ if(id != null) {
992
+ io.to("" + id).emit("token", token);
993
+ }
994
+ }, (err) => {
995
+ res.render(__dirname + "/views/authentication.html", {
996
+ publicKey: config.recaptchaPublicKey,
997
+ enableRecaptcha: config.enableRecaptcha,
998
+ errorRecaptcha: err == "INVALID_RECAPTCHA",
999
+ errorUsername: err == "BAD_USERNAME",
1000
+ errorUsernameBanned: err == "BANNED_USERNAME",
1001
+ errorUsernameAlreadyInUse: err == "USERNAME_ALREADY_IN_USE",
1002
+ success: false,
1003
+ authent: false,
1004
+ locale: i18n.getLocale(req),
1005
+ min: config.minCharactersUsername,
1006
+ max: config.maxCharactersUsername,
1007
+ enableMaxTimeGame: config.enableMaxTimeGame,
1008
+ maxTimeGame: config.maxTimeGame
1009
+ });
1010
+ });
1011
+ }
1012
+ });
1013
+ } else {
1014
+ res.end();
1015
+ }
1016
+ });
1017
+
1018
+ app.get("/rooms", function(req, res) {
1019
+ const callbackName = req.query.callback;
1020
+ res.charset = "UTF-8";
1021
+
1022
+ if(callbackName != null) {
1023
+ res.end(entities.encode(req.query.callback) + "(" + JSON.stringify(getRoomsData()) + ");");
1024
+ } else {
1025
+ res.json(getRoomsData());
1026
+ }
1027
+ });
1028
+
1029
+ // Admin panel
1030
+ function kickUser(socketId, token) {
1031
+ if(io.of("/").sockets.get(socketId)) {
1032
+ logger.info("user kicked (socket: " + socketId + ") - ip: " + getIPSocketIO(io.of("/").sockets.get(socketId).handshake));
1033
+ io.of("/").sockets.get(socketId).disconnect(true);
1034
+ invalidateUserToken(token);
1035
+ }
1036
+ }
1037
+
1038
+ function kickUsername(username) {
1039
+ tokens.forEach(token => {
1040
+ jwt.verify(token, jsonWebTokenSecretKey, function(err, data) {
1041
+ if(!err && data && data.username && data.username == username) {
1042
+ invalidateUserToken(token);
1043
+ }
1044
+ });
1045
+ });
1046
+ }
1047
+
1048
+ function invalidateUserToken(token) {
1049
+ invalidatedUserTokens.push(token);
1050
+
1051
+ for(let i = tokens.length - 1; i >= 0; i--) {
1052
+ if(tokens[i] == token) {
1053
+ tokens.splice(i, 1);
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ function banUserIP(socketId) {
1059
+ if(io.of("/").sockets.get(socketId)) {
1060
+ let ip = getIPSocketIO(io.of("/").sockets.get(socketId).handshake);
1061
+
1062
+ if(ip && ip.substr(0, 7) == "::ffff:") {
1063
+ ip = ip.substr(7, ip.length);
1064
+ }
1065
+
1066
+ if(ip) {
1067
+ config.ipBan.push(ip);
1068
+ logger.info("user banned (socket: " + socketId + ") - ip: " + ip);
1069
+ }
1070
+
1071
+ updateConfigToFile();
1072
+ }
1073
+ }
1074
+
1075
+ function banUserName(token) {
1076
+ jwt.verify(token, jsonWebTokenSecretKey, function(err, data) {
1077
+ if(!err && data) {
1078
+ if(data.username) {
1079
+ config.usernameBan.push(data.username);
1080
+ logger.info("username banned (" + data.username + ")");
1081
+ }
1082
+
1083
+ updateConfigToFile();
1084
+ }
1085
+ });
1086
+ }
1087
+
1088
+ function unbanUsername(value) {
1089
+ config.usernameBan = config.usernameBan.filter(username => username != value);
1090
+ logger.info("username unbanned (" + value + ")");
1091
+ updateConfigToFile();
1092
+ }
1093
+
1094
+ function unbanIP(value) {
1095
+ config.ipBan = config.ipBan.filter(ip => ip != value);
1096
+ logger.info("ip unbanned (" + value + ")");
1097
+ updateConfigToFile();
1098
+ }
1099
+
1100
+ function manualUsernameBan(value) {
1101
+ config.usernameBan.push(value);
1102
+ logger.info("username banned (" + value + ")");
1103
+ updateConfigToFile();
1104
+ }
1105
+
1106
+ function manualIPBan(value) {
1107
+ config.ipBan.push(value);
1108
+ logger.info("ip banned (" + value + ")");
1109
+ updateConfigToFile();
1110
+ }
1111
+
1112
+ function resetLog() {
1113
+ fs.writeFileSync(config.logFile, "", "UTF-8");
1114
+ logger.info("log file reseted");
1115
+ }
1116
+
1117
+ function resetErrorLog() {
1118
+ fs.writeFileSync(config.errorLogFile, "", "UTF-8");
1119
+ logger.info("error log file reseted");
1120
+ }
1121
+
1122
+ function updateConfig(value) {
1123
+ try {
1124
+ const parsed = JSON.parse(value);
1125
+ const keys = Object.keys(parsed);
1126
+
1127
+ for(let i = 0; i < keys.length; i++) {
1128
+ config[keys[i]] = parsed[keys[i]];
1129
+ }
1130
+
1131
+ updateConfigToFile();
1132
+ logger.info("updated config file");
1133
+ } catch(e) {
1134
+ logger.info("update config file - exception: " + e);
1135
+ }
1136
+ }
1137
+
1138
+ function verifyFormAuthenticationAdmin(body) {
1139
+ return new Promise((resolve, reject) => {
1140
+ verifyRecaptcha(body["g-recaptcha-response"]).then(() => {
1141
+ const username = body["username"];
1142
+ const password = body["password"];
1143
+
1144
+ const accounts = config.adminAccounts;
1145
+
1146
+ if(accounts) {
1147
+ const usernames = Object.keys(accounts);
1148
+
1149
+ if(usernames.includes(username)) {
1150
+ const hashPassword = accounts[username]["password"];
1151
+ const enteredPasswordHash = require("crypto").createHash("sha512").update(password).digest("hex");
1152
+
1153
+ if(hashPassword === enteredPasswordHash) {
1154
+ resolve();
1155
+ } else {
1156
+ reject("INVALID");
1157
+ }
1158
+ } else {
1159
+ reject("INVALID");
1160
+ }
1161
+ }
1162
+ }, () => {
1163
+ reject("INVALID_RECAPTCHA");
1164
+ });
1165
+ });
1166
+ }
1167
+
1168
+ const csrfProtection = csrf({ cookie: true });
1169
+
1170
+ app.get("/admin", csrfProtection, function(req, res) {
1171
+ if(req.cookies) {
1172
+ jwt.verify(req.cookies.tokenAdmin, jsonWebTokenSecretKeyAdmin, function(err, data) {
1173
+ if(invalidatedAdminTokens.includes(req.cookies.tokenAdmin)) err = true;
1174
+
1175
+ const usernames = Object.keys(config.adminAccounts);
1176
+ const authenticated = !err && data && data.username && usernames.includes(data.username);
1177
+ let role = "none";
1178
+
1179
+ if(authenticated) {
1180
+ role = config.adminAccounts[data.username]["role"] || "moderator";
1181
+ }
1182
+
1183
+ fs.readFile(config.logFile, "UTF-8", function(e1, logFile) {
1184
+ fs.readFile(config.errorLogFile, "UTF-8", function(e2, errorLogFile) {
1185
+ res.render(__dirname + "/views/admin.html", {
1186
+ publicKey: config.recaptchaPublicKey,
1187
+ enableRecaptcha: config.enableRecaptcha,
1188
+ authent: authenticated,
1189
+ role: role,
1190
+ username: authenticated ? data.username : "",
1191
+ success: false,
1192
+ errorAuthent: false,
1193
+ errorRecaptcha: false,
1194
+ locale: i18n.getLocale(req),
1195
+ games: games,
1196
+ io: io,
1197
+ config: config,
1198
+ csrfToken: req.csrfToken(),
1199
+ serverLog: logFile,
1200
+ errorLog: errorLogFile,
1201
+ getIPSocketIO: getIPSocketIO
1202
+ });
1203
+ });
1204
+ });
1205
+ });
1206
+ } else {
1207
+ res.end();
1208
+ }
1209
+ });
1210
+
1211
+ function adminAction(req, res, action) {
1212
+ if(req.cookies) {
1213
+ jwt.verify(req.cookies.tokenAdmin, jsonWebTokenSecretKeyAdmin, function(err, data) {
1214
+ if(invalidatedAdminTokens.includes(req.cookies.tokenAdmin)) err = true;
1215
+
1216
+ const usernames = Object.keys(config.adminAccounts);
1217
+ const authenticated = !err && data && data.username && usernames.includes(data.username);
1218
+
1219
+ if(authenticated) {
1220
+ const username = data.username;
1221
+ const role = config.adminAccounts[username]["role"] || "moderator";
1222
+
1223
+ if(action == "disconnect") {
1224
+ invalidatedAdminTokens.push(req.cookies.tokenAdmin);
1225
+ res.cookie("tokenAdmin", { expires: -1 });
1226
+ res.redirect("/admin");
1227
+ return;
1228
+ } else if(action) {
1229
+ const socket = req.body.socket;
1230
+ const token = req.body.token;
1231
+ const value = req.body.value;
1232
+
1233
+ switch(action) {
1234
+ case "kick":
1235
+ kickUser(socket, token);
1236
+ break;
1237
+ case "banIP":
1238
+ if(value) {
1239
+ manualIPBan(value);
1240
+ } else {
1241
+ banUserIP(socket);
1242
+ kickUser(socket, token);
1243
+ }
1244
+ break;
1245
+ case "banUserName":
1246
+ if(value) {
1247
+ manualUsernameBan(value);
1248
+ kickUsername(value);
1249
+ } else {
1250
+ banUserName(token);
1251
+ kickUser(socket, token);
1252
+ }
1253
+ break;
1254
+ case "banIPUserName":
1255
+ banUserIP(socket);
1256
+ banUserName(token);
1257
+ kickUser(socket, token);
1258
+ break;
1259
+ case "unbanUsername":
1260
+ unbanUsername(value);
1261
+ break;
1262
+ case "unbanIP":
1263
+ unbanIP(value);
1264
+ break;
1265
+ case "resetLog":
1266
+ if(role === "administrator") resetLog();
1267
+ break;
1268
+ case "resetErrorLog":
1269
+ if(role === "administrator") resetErrorLog();
1270
+ break;
1271
+ case "updateConfig":
1272
+ if(role === "administrator") updateConfig(value);
1273
+ break;
1274
+ }
1275
+
1276
+ res.redirect("/admin");
1277
+ return;
1278
+ }
1279
+ }
1280
+
1281
+ res.redirect("/admin");
1282
+ return;
1283
+ });
1284
+ } else {
1285
+ res.end();
1286
+ }
1287
+ }
1288
+
1289
+ const jsonParser = bodyParser.json();
1290
+
1291
+ app.post("/admin/:action", jsonParser, csrfProtection, function(req, res) {
1292
+ adminAction(req, res, req.params.action);
1293
+ });
1294
+
1295
+ app.use(function (err, req, res, next) {
1296
+ if(err.code !== "EBADCSRFTOKEN") return next(err);
1297
+ res.status(403);
1298
+ res.send("Error");
1299
+ });
1300
+
1301
+ const adminRateLimiter = rateLimit({
1302
+ windowMs: config.authentWindowMs,
1303
+ max: config.authentMaxRequest
1304
+ });
1305
+
1306
+ app.post("/admin", adminRateLimiter, function(req, res) {
1307
+ if(req.cookies) {
1308
+ jwt.verify(req.cookies.tokenAdmin, jsonWebTokenSecretKeyAdmin, function(err, data) {
1309
+ if(invalidatedAdminTokens.includes(req.cookies.tokenAdmin)) res = true;
1310
+
1311
+ if(err) {
1312
+ verifyFormAuthenticationAdmin(req.body).then(() => {
1313
+ const username = req.body["username"];
1314
+
1315
+ const token = jwt.sign({
1316
+ username: username
1317
+ }, jsonWebTokenSecretKeyAdmin, { expiresIn: config.authenticationTime / 1000 });
1318
+
1319
+ res.cookie("tokenAdmin", token, { expires: new Date(Date.now() + config.authenticationTime), httpOnly: true, sameSite: "strict", secure: (req.protocol == "https" ? true : false) });
1320
+ res.redirect("/admin");
1321
+ logger.info("admin authent - username: " + username + " - ip: " + req.ip);
1322
+ return;
1323
+ }, (err) => {
1324
+ res.render(__dirname + "/views/admin.html", {
1325
+ publicKey: config.recaptchaPublicKey,
1326
+ enableRecaptcha: config.enableRecaptcha,
1327
+ authent: false,
1328
+ errorAuthent: true,
1329
+ errorRecaptcha: err == "INVALID_RECAPTCHA",
1330
+ locale: i18n.getLocale(req),
1331
+ games: null,
1332
+ io: null,
1333
+ config: null,
1334
+ csrfToken: null,
1335
+ serverLog: null,
1336
+ errorLog: null,
1337
+ getIPSocketIO: getIPSocketIO
1338
+ });
1339
+ });
1340
+ }
1341
+ });
1342
+ } else {
1343
+ res.end();
1344
+ }
1345
+ });
1346
+
1347
+ io.use(ioCookieParser());
1348
+
1349
+ function getIPSocketIO(req) {
1350
+ let ipAddress;
1351
+
1352
+ if(req && req.headers) {
1353
+ const forwardedIpsStr = req.headers["x-forwarded-for"];
1354
+
1355
+ if(forwardedIpsStr && forwardedIpsStr !== undefined && config.proxyMode) {
1356
+ const forwardedIps = forwardedIpsStr.split(",");
1357
+ ipAddress = forwardedIps[0];
1358
+ }
1359
+
1360
+ if(!ipAddress) {
1361
+ ipAddress = req.address;
1362
+ }
1363
+ }
1364
+
1365
+ return ipAddress;
1366
+ }
1367
+
1368
+ function checkAuthentication(token) {
1369
+ return new Promise((resolve, reject) => {
1370
+ if(!config.enableAuthentication) {
1371
+ resolve();
1372
+ } else {
1373
+ if(token && invalidatedUserTokens.includes(token)) reject();
1374
+
1375
+ jwt.verify(token, jsonWebTokenSecretKey, function(err, data) {
1376
+ if(!err) {
1377
+ resolve(token);
1378
+ } else {
1379
+ reject();
1380
+ }
1381
+ });
1382
+ }
1383
+ });
1384
+ }
1385
+
1386
+ function checkAuthenticationSocket(socket) {
1387
+ return checkAuthentication(socket.handshake.auth.token || socket.handshake.query.token || socket.request.cookies.token);
1388
+ }
1389
+
1390
+ function checkAuthenticationExpress(req) {
1391
+ return checkAuthentication(req.cookies.token);
1392
+ }
1393
+
1394
+ io.use(function(socket, next) {
1395
+ ipBanned(getIPSocketIO(socket.handshake)).then(() => {
1396
+ next(new Error(GameConstants.Error.BANNED));
1397
+ }, () => {
1398
+ next();
1399
+ });
1400
+ });
1401
+
1402
+ io.of("/rooms").on("connection", function(socket) {
1403
+ checkAuthenticationSocket(socket).then(() => {
1404
+ socket.emit("rooms", {
1405
+ rooms: getRoomsData(),
1406
+ serverVersion: config.version,
1407
+ version: GameConstants.Setting.APP_VERSION,
1408
+ settings: {
1409
+ maxRooms: config.maxRooms,
1410
+ minGridSize: config.minGridSize,
1411
+ maxGridSize: config.maxGridSize,
1412
+ minSpeed: config.minSpeed,
1413
+ maxSpeed: config.maxSpeed,
1414
+ enableAI: config.enableAI
1415
+ }
1416
+ });
1417
+ }, () => {
1418
+ socket.emit("authent", GameConstants.Error.AUTHENTICATION_REQUIRED);
1419
+ });
1420
+ });
1421
+
1422
+ io.of("/createRoom").on("connection", function(socket) {
1423
+ socket.on("create", function(data) {
1424
+ checkAuthenticationSocket(socket).then(() => {
1425
+ createRoom(data, socket);
1426
+ }, () => {
1427
+ socket.emit("authent", GameConstants.Error.AUTHENTICATION_REQUIRED);
1428
+ });
1429
+ });
1430
+ });
1431
+
1432
+ io.on("connection", function(socket) {
1433
+ checkAuthenticationSocket(socket).then((token) => {
1434
+ socket.emit("authent", GameConstants.GameState.AUTHENTICATION_SUCCESS);
1435
+ tokens.push(token);
1436
+
1437
+ socket.on("join-room", function(data) {
1438
+ const code = data.code;
1439
+ const version = data.version;
1440
+ const game = games[code];
1441
+
1442
+ if(game != null && !Player.containsId(game.players, socket.id) && !Player.containsId(game.spectators, socket.id) && !Player.containsToken(game.players, token) && !Player.containsToken(game.spectators, token) && !Player.containsTokenAllGames(token) && !Player.containsIdAllGames(socket.id)) {
1443
+ socket.join("room-" + code);
1444
+
1445
+ if(game.players.length + game.numberAIToAdd >= getMaxPlayers(code) || game.started) {
1446
+ game.spectators.push(new Player(token, socket.id, null, false, version));
1447
+ } else {
1448
+ game.players.push(new Player(token, socket.id, null, false, version));
1449
+ }
1450
+
1451
+ socket.emit("join-room", {
1452
+ success: true
1453
+ });
1454
+
1455
+ socket.once("start", () => {
1456
+ if(Player.containsId(game.players, socket.id)) {
1457
+ Player.getPlayer(game.players, socket.id).ready = true;
1458
+ }
1459
+
1460
+ if(game.started) {
1461
+ socket.emit("init", {
1462
+ "paused": game.game.paused,
1463
+ "isReseted": game.game.isReseted,
1464
+ "exited": game.game.exited,
1465
+ "snakes": copySnakes(game.game.snakes),
1466
+ "grid": game.game.grid,
1467
+ "numFruit": game.game.numFruit,
1468
+ "ticks": game.game.ticks,
1469
+ "scoreMax": game.game.scoreMax,
1470
+ "gameOver": game.game.gameOver,
1471
+ "gameFinished": game.game.gameFinished,
1472
+ "gameMazeWin": game.game.gameMazeWin,
1473
+ "starting": game.game.starting,
1474
+ "initialSpeed": game.game.initialSpeed,
1475
+ "speed": game.game.speed,
1476
+ "offsetFrame": game.game.speed * GameConstants.Setting.TIME_MULTIPLIER,
1477
+ "confirmReset": false,
1478
+ "confirmExit": false,
1479
+ "getInfos": false,
1480
+ "getInfosGame": false,
1481
+ "errorOccurred": game.game.errorOccurred,
1482
+ "timerToDisplay": config.enableMaxTimeGame ? (config.maxTimeGame - (Date.now() - game.timeStart)) / 1000 : -1,
1483
+ "countBeforePlay": game.game.countBeforePlay,
1484
+ "aiStuck": game.game.aiStuck,
1485
+ "precAiStuck": false
1486
+ });
1487
+ } else {
1488
+ socket.emit("init", {
1489
+ "enablePause": game.game.enablePause,
1490
+ "enableRetry": game.game.enableRetry,
1491
+ "progressiveSpeed": game.game.progressiveSpeed,
1492
+ "offsetFrame": game.game.speed * GameConstants.Setting.TIME_MULTIPLIER
1493
+ });
1494
+ }
1495
+
1496
+ gameMatchmaking(game, code);
1497
+
1498
+ socket.on("start", () => {
1499
+ socket.emit("start", {
1500
+ "paused": false
1501
+ });
1502
+ });
1503
+ });
1504
+
1505
+ socket.once("exit", () => {
1506
+ exitGame(game, socket, code);
1507
+ });
1508
+
1509
+ socket.once("kill", () => {
1510
+ exitGame(game, socket, code);
1511
+ });
1512
+
1513
+ socket.on("key", function(key) {
1514
+ if(game != null && Player.containsId(game.players, socket.id) && Player.getPlayer(game.players, socket.id).snake) {
1515
+ Player.getPlayer(game.players, socket.id).snake.lastKey = key;
1516
+ sendStatus(code);
1517
+ }
1518
+ });
1519
+
1520
+ socket.on("pause", () => {
1521
+ socket.emit("pause", {
1522
+ "paused": true
1523
+ });
1524
+ });
1525
+
1526
+ socket.on("reset", () => {
1527
+ if(!game.started) {
1528
+ gameMatchmaking(game, code);
1529
+
1530
+ socket.emit("reset", {
1531
+ "gameOver": false,
1532
+ "gameFinished": false,
1533
+ "scoreMax": false,
1534
+ "gameMazeWin": false
1535
+ });
1536
+ }
1537
+ });
1538
+
1539
+ socket.once("error", () => {
1540
+ exitGame(game, socket, code);
1541
+ });
1542
+
1543
+ socket.once("disconnect", () => {
1544
+ exitGame(game, socket, code);
1545
+ });
1546
+
1547
+ socket.on("forceStart", () => {
1548
+ if(game != null && Player.containsId(game.players, socket.id) && game.players[0].id == socket.id && !game.started) {
1549
+ startGame(code);
1550
+ }
1551
+ });
1552
+
1553
+ logger.info("join room (code: " + code + ") - username: " + Player.getUsernameSocket(socket) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
1554
+ } else {
1555
+ if(games[code] == null) {
1556
+ socket.emit("join-room", {
1557
+ success: false,
1558
+ errorCode: GameConstants.Error.ROOM_NOT_FOUND
1559
+ });
1560
+ } else if(Player.containsId(game.players, socket.id) || Player.containsId(game.spectators, socket.id) || Player.containsToken(game.players, token) || Player.containsToken(game.spectators, token)) {
1561
+ socket.emit("join-room", {
1562
+ success: false,
1563
+ errorCode: GameConstants.Error.ROOM_ALREADY_JOINED
1564
+ });
1565
+ } else if(Player.containsTokenAllGames(token) || Player.containsIdAllGames(socket.id)) {
1566
+ socket.emit("join-room", {
1567
+ success: false,
1568
+ errorCode: GameConstants.Error.ALREADY_CREATED_ROOM
1569
+ });
1570
+ }
1571
+ }
1572
+ });
1573
+ }, () => {
1574
+ socket.emit("authent", GameConstants.Error.AUTHENTICATION_REQUIRED);
1575
+ });
1576
+ });
1577
+
1578
+ http.listen(config.port, function(){
1579
+ console.log("listening on *:" + config.port);
1580
+ });