snakeia-server 1.2.7 → 2.0.0
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/.drone.yml +1 -1
- package/GameEngineMultithreading.js +14 -0
- package/Player.js +22 -18
- package/README.md +90 -8
- package/config/default.json +7 -2
- package/locales/en.json +2 -1
- package/locales/fr.json +2 -1
- package/package.json +15 -6
- package/server.js +782 -549
- package/views/admin.html +12 -2
- package/views/authentication.html +5 -1
package/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright (C) 2020 Eliastik (eliastiksofts.com)
|
|
2
|
+
* Copyright (C) 2020-2026 Eliastik (eliastiksofts.com)
|
|
3
3
|
*
|
|
4
4
|
* This file is part of "SnakeIA Server".
|
|
5
5
|
*
|
|
@@ -16,25 +16,32 @@
|
|
|
16
16
|
* You should have received a copy of the GNU General Public License
|
|
17
17
|
* along with "SnakeIA Server". If not, see <http://www.gnu.org/licenses/>.
|
|
18
18
|
*/
|
|
19
|
-
const express
|
|
20
|
-
const app
|
|
21
|
-
const fs
|
|
22
|
-
const httpLib
|
|
23
|
-
const httpsLib
|
|
24
|
-
let server
|
|
25
|
-
let io
|
|
26
|
-
const entities
|
|
27
|
-
const ejs
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
19
|
+
const express = require("express");
|
|
20
|
+
const app = express();
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const httpLib = require("http");
|
|
23
|
+
const httpsLib = require("https");
|
|
24
|
+
let server = null;
|
|
25
|
+
let io = null;
|
|
26
|
+
const entities = require("html-entities");
|
|
27
|
+
const ejs = require("ejs");
|
|
28
|
+
const { SignJWT,
|
|
29
|
+
jwtVerify,
|
|
30
|
+
decodeJwt } = require("jose");
|
|
31
|
+
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
|
|
32
|
+
const cookieParser = require("cookie-parser");
|
|
33
|
+
const ioCookieParser = require("socket.io-cookie-parser");
|
|
34
|
+
const i18n = require("i18n");
|
|
35
|
+
const rateLimit = require("express-rate-limit");
|
|
36
|
+
const winston = require("winston");
|
|
37
|
+
const { doubleCsrf } = require("csrf-csrf");
|
|
38
|
+
const bodyParser = require("body-parser");
|
|
39
|
+
const node_config = require("config");
|
|
40
|
+
const { randomUUID,
|
|
41
|
+
randomBytes,
|
|
42
|
+
createSecretKey,
|
|
43
|
+
createHash } = require("crypto");
|
|
44
|
+
const semver = require("semver");
|
|
38
45
|
|
|
39
46
|
process.env["ALLOW_CONFIG_MUTATIONS"] = true;
|
|
40
47
|
let config = node_config.get("ServerConfig"); // Server configuration (see default config file config.json)
|
|
@@ -44,15 +51,26 @@ const configSources = node_config.util.getConfigSources();
|
|
|
44
51
|
const configFile = configSources[configSources.length - 1].name;
|
|
45
52
|
|
|
46
53
|
config.port = process.env.PORT || config.port;
|
|
47
|
-
|
|
48
|
-
const
|
|
54
|
+
|
|
55
|
+
const jsonWebTokenSecretKey = createSecretKey(
|
|
56
|
+
Buffer.from(config.jsonWebTokenSecretKey?.trim() || randomBytes(32))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const jsonWebTokenSecretKeyAdmin = createSecretKey(
|
|
60
|
+
Buffer.from(config.jsonWebTokenSecretKeyAdmin?.trim() || randomBytes(32))
|
|
61
|
+
);
|
|
49
62
|
|
|
50
63
|
const productionMode = process.env.NODE_ENV === "production";
|
|
51
64
|
|
|
52
65
|
// Update config to file
|
|
53
66
|
function updateConfigToFile() {
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(configFile, JSON.stringify({ "ServerConfig": config }, null, 4), "UTF-8");
|
|
69
|
+
config = node_config.get("ServerConfig");
|
|
70
|
+
logger.info("updated config file");
|
|
71
|
+
} catch(e) {
|
|
72
|
+
logger.error("failed to update config file", e);
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
// Logging
|
|
@@ -114,7 +132,6 @@ io = require("socket.io")(server, {
|
|
|
114
132
|
|
|
115
133
|
// Game modules
|
|
116
134
|
const snakeia = require("snakeia");
|
|
117
|
-
const { randomUUID } = require("crypto");
|
|
118
135
|
const Snake = snakeia.Snake;
|
|
119
136
|
const Grid = snakeia.Grid;
|
|
120
137
|
const GameConstants = snakeia.GameConstants;
|
|
@@ -122,9 +139,10 @@ const GameEngine = config.enableMultithreading ? require("./GameEngineMultit
|
|
|
122
139
|
const Player = require("./Player");
|
|
123
140
|
|
|
124
141
|
const games = {}; // Contains all the games processed by the server
|
|
125
|
-
const tokens =
|
|
126
|
-
const
|
|
127
|
-
const
|
|
142
|
+
const tokens = new Map(); // User tokens
|
|
143
|
+
const socketSessions = new Map();
|
|
144
|
+
const invalidatedUserTokens = new Set(); // Invalidated user tokens
|
|
145
|
+
const invalidatedAdminTokens = new Set(); // Invalidated admin tokens
|
|
128
146
|
|
|
129
147
|
function getRoomsData() {
|
|
130
148
|
const rooms = [];
|
|
@@ -164,20 +182,30 @@ function getRoomsData() {
|
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
function getRandomRoomKey() {
|
|
167
|
-
let
|
|
185
|
+
let roomKey;
|
|
168
186
|
|
|
169
187
|
do {
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
const length = 8;
|
|
189
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
190
|
+
const charsLength = chars.length;
|
|
191
|
+
|
|
192
|
+
const bytes = randomBytes(length);
|
|
193
|
+
|
|
194
|
+
roomKey = "";
|
|
195
|
+
|
|
196
|
+
for(let i = 0; i < length; i++) {
|
|
197
|
+
roomKey += chars[bytes[i] % charsLength];
|
|
198
|
+
}
|
|
199
|
+
} while(roomKey in games);
|
|
172
200
|
|
|
173
|
-
return
|
|
201
|
+
return roomKey;
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
function generateRandomJsonWebTokenSecretKey(precValue) {
|
|
177
205
|
let key;
|
|
178
206
|
|
|
179
207
|
do {
|
|
180
|
-
key =
|
|
208
|
+
key = randomBytes(256).toString("base64");
|
|
181
209
|
} while(precValue && precValue == key);
|
|
182
210
|
|
|
183
211
|
return key;
|
|
@@ -206,8 +234,8 @@ function getMaxPlayers(code) {
|
|
|
206
234
|
return Math.min(config.maxPlayers, Math.max(Math.round(numberEmptyCases / 5), 2));
|
|
207
235
|
}
|
|
208
236
|
|
|
209
|
-
function createRoom(data, socket) {
|
|
210
|
-
if(Object.keys(games).filter(key => games[key] != null).length < config.maxRooms && !Player.containsTokenAllGames(socket
|
|
237
|
+
async function createRoom(data, socket) {
|
|
238
|
+
if(Object.keys(games).filter(key => games[key] != null).length < config.maxRooms && !Player.containsTokenAllGames(Player.getSocketToken(socket), games) && !Player.containsIdAllGames(socket.id, games)) {
|
|
211
239
|
let heightGrid = 20;
|
|
212
240
|
let widthGrid = 20;
|
|
213
241
|
let borderWalls = false;
|
|
@@ -218,13 +246,13 @@ function createRoom(data, socket) {
|
|
|
218
246
|
let validSettings = true;
|
|
219
247
|
let privateGame = false;
|
|
220
248
|
|
|
221
|
-
if(data.heightGrid == null || isNaN(data.heightGrid) || data.heightGrid < config.minGridSize || data.heightGrid > config.maxGridSize) {
|
|
249
|
+
if(data.heightGrid == null || isNaN(data.heightGrid) || !Number.isInteger(data.heightGrid) || data.heightGrid < config.minGridSize || data.heightGrid > config.maxGridSize) {
|
|
222
250
|
validSettings = false;
|
|
223
251
|
} else {
|
|
224
252
|
heightGrid = data.heightGrid;
|
|
225
253
|
}
|
|
226
254
|
|
|
227
|
-
if(data.widthGrid == null || isNaN(data.widthGrid) || data.widthGrid < config.minGridSize || data.widthGrid > config.maxGridSize) {
|
|
255
|
+
if(data.widthGrid == null || isNaN(data.widthGrid) || !Number.isInteger(data.widthGrid) || data.widthGrid < config.minGridSize || data.widthGrid > config.maxGridSize) {
|
|
228
256
|
validSettings = false;
|
|
229
257
|
} else {
|
|
230
258
|
widthGrid = data.widthGrid;
|
|
@@ -249,12 +277,12 @@ function createRoom(data, socket) {
|
|
|
249
277
|
}
|
|
250
278
|
|
|
251
279
|
if(data.speed == "custom") {
|
|
252
|
-
if(data.customSpeed == null || isNaN(data.customSpeed) || data.customSpeed < config.minSpeed || data.customSpeed > config.maxSpeed) {
|
|
280
|
+
if(data.customSpeed == null || isNaN(data.customSpeed) || !Number.isInteger(data.customSpeed) || data.customSpeed < config.minSpeed || data.customSpeed > config.maxSpeed) {
|
|
253
281
|
validSettings = false;
|
|
254
282
|
} else {
|
|
255
283
|
speed = data.customSpeed;
|
|
256
284
|
}
|
|
257
|
-
} else if(data.speed == null || isNaN(data.speed) || data.speed < config.minSpeed || data.speed > config.maxSpeed) {
|
|
285
|
+
} else if(data.speed == null || isNaN(data.speed) || !Number.isInteger(data.speed) || data.speed < config.minSpeed || data.speed > config.maxSpeed) {
|
|
258
286
|
validSettings = false;
|
|
259
287
|
} else {
|
|
260
288
|
speed = data.speed;
|
|
@@ -294,13 +322,14 @@ function createRoom(data, socket) {
|
|
|
294
322
|
started: false,
|
|
295
323
|
alreadyInit: false,
|
|
296
324
|
timeoutPlay: null,
|
|
325
|
+
timeoutMatchmaking: null,
|
|
297
326
|
timeStart: null,
|
|
298
327
|
timeoutMaxTimePlay: null
|
|
299
328
|
};
|
|
300
329
|
|
|
301
330
|
games[code].numberAIToAdd = enableAI ? Math.round(getMaxPlayers(code) / 2 - 1) : 0;
|
|
302
331
|
|
|
303
|
-
logger.info("room creation (code: " + code + ") - username: " + (Player.getUsernameSocket(socket)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id, {
|
|
332
|
+
logger.info("room creation (code: " + code + ") - username: " + (await Player.getUsernameSocket(socket, jsonWebTokenSecretKey)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id, {
|
|
304
333
|
"widthGrid": widthGrid,
|
|
305
334
|
"heightGrid": heightGrid,
|
|
306
335
|
"generateWalls": generateWalls,
|
|
@@ -326,7 +355,7 @@ function createRoom(data, socket) {
|
|
|
326
355
|
});
|
|
327
356
|
}
|
|
328
357
|
} else if(socket != null) {
|
|
329
|
-
if(Player.containsTokenAllGames(socket
|
|
358
|
+
if(Player.containsTokenAllGames(Player.getSocketToken(socket), games) || Player.containsIdAllGames(socket.id, games)) {
|
|
330
359
|
socket.emit("process", {
|
|
331
360
|
success: false,
|
|
332
361
|
code: null,
|
|
@@ -358,11 +387,11 @@ function copySnakes(snakes) {
|
|
|
358
387
|
snakeCopy.aiLevel = snake.aiLevel;
|
|
359
388
|
|
|
360
389
|
if(snake.lastTail) {
|
|
361
|
-
snakeCopy.lastTail =
|
|
390
|
+
snakeCopy.lastTail = structuredClone(snake.lastTail);
|
|
362
391
|
}
|
|
363
392
|
|
|
364
393
|
if(snake.lastHead) {
|
|
365
|
-
snakeCopy.lastHead =
|
|
394
|
+
snakeCopy.lastHead = structuredClone(snake.lastHead);
|
|
366
395
|
}
|
|
367
396
|
|
|
368
397
|
snakeCopy.lastTailMoved = snake.lastTailMoved;
|
|
@@ -371,7 +400,7 @@ function copySnakes(snakes) {
|
|
|
371
400
|
snakeCopy.player = snake.player;
|
|
372
401
|
|
|
373
402
|
if(snake.queue) {
|
|
374
|
-
snakeCopy.queue =
|
|
403
|
+
snakeCopy.queue = structuredClone(snake.queue);
|
|
375
404
|
}
|
|
376
405
|
|
|
377
406
|
snakeCopy.score = snake.score;
|
|
@@ -416,6 +445,8 @@ function setupRoom(code) {
|
|
|
416
445
|
"confirmExit": false,
|
|
417
446
|
"getInfos": false,
|
|
418
447
|
"getInfosGame": false,
|
|
448
|
+
"getInfosControls": false,
|
|
449
|
+
"getInfosGoal": false,
|
|
419
450
|
"errorOccurred": game.errorOccurred,
|
|
420
451
|
"timerToDisplay": config.enableMaxTimeGame ? (config.maxTimeGame - (Date.now() - game.timeStart)) / 1000 : -1,
|
|
421
452
|
"aiStuck": game.aiStuck,
|
|
@@ -435,6 +466,8 @@ function setupRoom(code) {
|
|
|
435
466
|
"confirmExit": false,
|
|
436
467
|
"getInfos": false,
|
|
437
468
|
"getInfosGame": false,
|
|
469
|
+
"getInfosControls": false,
|
|
470
|
+
"getInfosGoal": false,
|
|
438
471
|
"errorOccurred": game.errorOccurred,
|
|
439
472
|
"searchingPlayers": false
|
|
440
473
|
});
|
|
@@ -447,6 +480,8 @@ function setupRoom(code) {
|
|
|
447
480
|
"confirmExit": false,
|
|
448
481
|
"getInfos": false,
|
|
449
482
|
"getInfosGame": false,
|
|
483
|
+
"getInfosControls": false,
|
|
484
|
+
"getInfosGoal": false,
|
|
450
485
|
"errorOccurred": game.errorOccurred
|
|
451
486
|
});
|
|
452
487
|
});
|
|
@@ -457,6 +492,8 @@ function setupRoom(code) {
|
|
|
457
492
|
"confirmExit": false,
|
|
458
493
|
"getInfos": false,
|
|
459
494
|
"getInfosGame": false,
|
|
495
|
+
"getInfosControls": false,
|
|
496
|
+
"getInfosGoal": false,
|
|
460
497
|
"errorOccurred": game.errorOccurred
|
|
461
498
|
});
|
|
462
499
|
});
|
|
@@ -471,13 +508,22 @@ function setupRoom(code) {
|
|
|
471
508
|
"confirmExit": false,
|
|
472
509
|
"getInfos": false,
|
|
473
510
|
"getInfosGame": false,
|
|
511
|
+
"getInfosControls": false,
|
|
512
|
+
"getInfosGoal": false,
|
|
474
513
|
"errorOccurred": game.errorOccurred
|
|
475
514
|
});
|
|
476
515
|
|
|
477
516
|
if(games[code] != null) {
|
|
478
517
|
games[code].started = false;
|
|
479
518
|
games[code].searchingPlayers = true;
|
|
519
|
+
|
|
480
520
|
clearTimeout(games[code].timeoutMaxTimePlay);
|
|
521
|
+
clearTimeout(games[code].timeoutMatchmaking);
|
|
522
|
+
|
|
523
|
+
games[code].timeoutMatchmaking = setTimeout(() => {
|
|
524
|
+
gameMatchmaking(games[code], code);
|
|
525
|
+
io.to("room-" + code).emit("reset", { "gameOver": false, "gameFinished": false, "scoreMax": false, "gameMazeWin": false });
|
|
526
|
+
}, config.matchmakingWaitTime);
|
|
481
527
|
}
|
|
482
528
|
});
|
|
483
529
|
|
|
@@ -491,6 +537,8 @@ function setupRoom(code) {
|
|
|
491
537
|
"confirmExit": false,
|
|
492
538
|
"getInfos": false,
|
|
493
539
|
"getInfosGame": false,
|
|
540
|
+
"getInfosControls": false,
|
|
541
|
+
"getInfosGoal": false,
|
|
494
542
|
"errorOccurred": game.errorOccurred
|
|
495
543
|
});
|
|
496
544
|
});
|
|
@@ -507,6 +555,8 @@ function setupRoom(code) {
|
|
|
507
555
|
"confirmExit": false,
|
|
508
556
|
"getInfos": false,
|
|
509
557
|
"getInfosGame": false,
|
|
558
|
+
"getInfosControls": false,
|
|
559
|
+
"getInfosGoal": false,
|
|
510
560
|
"errorOccurred": game.errorOccurred
|
|
511
561
|
});
|
|
512
562
|
});
|
|
@@ -577,10 +627,9 @@ function cleanRooms() {
|
|
|
577
627
|
const game = games[keys[i]];
|
|
578
628
|
|
|
579
629
|
if(game != null) {
|
|
580
|
-
const players = Object.keys(game.players) + Object.keys(game.spectators);
|
|
581
|
-
const nb = players.length;
|
|
630
|
+
const players = Object.keys(game.players).length + Object.keys(game.spectators).length;
|
|
582
631
|
|
|
583
|
-
if(
|
|
632
|
+
if(players <= 0) {
|
|
584
633
|
toRemove.push(keys[i]);
|
|
585
634
|
|
|
586
635
|
if(game.gameEngine && game.gameEngine.kill) {
|
|
@@ -591,11 +640,16 @@ function cleanRooms() {
|
|
|
591
640
|
}
|
|
592
641
|
|
|
593
642
|
for(let i = 0; i < toRemove.length; i++) {
|
|
594
|
-
games[toRemove[i]]
|
|
643
|
+
delete games[toRemove[i]];
|
|
595
644
|
}
|
|
596
645
|
}
|
|
597
646
|
|
|
598
647
|
function gameMatchmaking(game, code) {
|
|
648
|
+
if(game.timeoutMatchmaking != null) {
|
|
649
|
+
clearTimeout(game.timeoutMatchmaking);
|
|
650
|
+
game.timeoutMatchmaking = null;
|
|
651
|
+
}
|
|
652
|
+
|
|
599
653
|
if(game != null && games[code] != null && games[code].searchingPlayers) {
|
|
600
654
|
let numberPlayers = game.players.length + games[code].numberAIToAdd;
|
|
601
655
|
|
|
@@ -661,6 +715,11 @@ async function startGame(code) {
|
|
|
661
715
|
game.timeoutPlay = null;
|
|
662
716
|
}
|
|
663
717
|
|
|
718
|
+
if(game.timeoutMatchmaking != null) {
|
|
719
|
+
clearTimeout(game.timeoutMatchmaking);
|
|
720
|
+
game.timeoutMatchmaking = null;
|
|
721
|
+
}
|
|
722
|
+
|
|
664
723
|
game.searchingPlayers = false;
|
|
665
724
|
game.started = true;
|
|
666
725
|
game.gameEngine.snakes = [];
|
|
@@ -745,9 +804,9 @@ function sendStatus(code) {
|
|
|
745
804
|
}
|
|
746
805
|
}
|
|
747
806
|
|
|
748
|
-
function exitGame(game, socket, code) {
|
|
807
|
+
async function exitGame(game, socket, code) {
|
|
749
808
|
if(game) {
|
|
750
|
-
logger.info("exit game (code: " + code + ") - username: " + Player.getUsernameSocket(socket) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
|
|
809
|
+
logger.info("exit game (code: " + code + ") - username: " + (await Player.getUsernameSocket(socket, jsonWebTokenSecretKey)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
|
|
751
810
|
|
|
752
811
|
if(Player.containsId(game.players, socket.id) && Player.getPlayer(game.players, socket.id).snake != null) {
|
|
753
812
|
Player.getPlayer(game.players, socket.id).snake.gameOver = true;
|
|
@@ -778,94 +837,92 @@ function ipBanned(ip) {
|
|
|
778
837
|
ip = ip.substr(7, ip.length);
|
|
779
838
|
}
|
|
780
839
|
|
|
781
|
-
return
|
|
782
|
-
config.ipBan.forEach(ipBanned => {
|
|
783
|
-
if(ipBanned == ip) {
|
|
784
|
-
resolve();
|
|
785
|
-
}
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
reject();
|
|
789
|
-
});
|
|
840
|
+
return config.ipBan.includes(ip);
|
|
790
841
|
}
|
|
791
842
|
|
|
792
843
|
function usernameBanned(username) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
});
|
|
844
|
+
for(const usernameBanned of config.usernameBan) {
|
|
845
|
+
if(username.toLowerCase().indexOf(usernameBanned.toLowerCase()) > -1) {
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
799
849
|
|
|
800
|
-
|
|
801
|
-
});
|
|
850
|
+
return false;
|
|
802
851
|
}
|
|
803
852
|
|
|
804
|
-
function
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
853
|
+
async function getUsernameToken(token, secretKey) {
|
|
854
|
+
try {
|
|
855
|
+
const { payload } = await jwtVerify(token, secretKey);
|
|
856
|
+
return payload.username ?? null;
|
|
857
|
+
} catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
809
861
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
} catch(e) {}
|
|
816
|
-
});
|
|
862
|
+
async function usernameAlreadyInUse(username) {
|
|
863
|
+
if(!tokens.has(username.toLowerCase())) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
817
866
|
|
|
818
|
-
|
|
819
|
-
|
|
867
|
+
try {
|
|
868
|
+
const token = tokens.get(username.toLowerCase());
|
|
869
|
+
const result = await getUsernameToken(token, jsonWebTokenSecretKey);
|
|
870
|
+
|
|
871
|
+
return result != null;
|
|
872
|
+
} catch(e) {
|
|
873
|
+
logger.error(e);
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
820
876
|
}
|
|
821
877
|
|
|
822
|
-
function verifyRecaptcha(response) {
|
|
878
|
+
async function verifyRecaptcha(response) {
|
|
823
879
|
if(config.enableRecaptcha && config.recaptchaPrivateKey && config.recaptchaPrivateKey.trim() != "" && config.recaptchaPublicKey && config.recaptchaPublicKey.trim() != "") {
|
|
824
880
|
const params = new URLSearchParams();
|
|
881
|
+
|
|
825
882
|
params.append("secret", config.recaptchaPrivateKey);
|
|
826
883
|
params.append("response", response);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
fetch(config.recaptchaApiUrl, {
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const fetchResponse = await fetch(config.recaptchaApiUrl, {
|
|
830
887
|
method: "POST",
|
|
831
888
|
body: params
|
|
832
|
-
}).then(res => res.json()).then(json => {
|
|
833
|
-
if(json && json.success) {
|
|
834
|
-
resolve();
|
|
835
|
-
} else {
|
|
836
|
-
reject();
|
|
837
|
-
}
|
|
838
889
|
});
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
890
|
+
|
|
891
|
+
const responseBody = await fetchResponse.json();
|
|
892
|
+
|
|
893
|
+
if(responseBody && responseBody.success) {
|
|
894
|
+
return Promise.resolve();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return Promise.reject();
|
|
898
|
+
} catch(e) {
|
|
899
|
+
return Promise.reject();
|
|
900
|
+
}
|
|
844
901
|
}
|
|
902
|
+
|
|
903
|
+
return Promise.resolve();
|
|
845
904
|
}
|
|
846
905
|
|
|
847
|
-
function verifyFormAuthentication(body) {
|
|
848
|
-
|
|
849
|
-
verifyRecaptcha(body["g-recaptcha-response"])
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
});
|
|
906
|
+
async function verifyFormAuthentication(body) {
|
|
907
|
+
try {
|
|
908
|
+
await verifyRecaptcha(body["g-recaptcha-response"]);
|
|
909
|
+
} catch {
|
|
910
|
+
throw "INVALID_RECAPTCHA";
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const username = body["username"];
|
|
914
|
+
|
|
915
|
+
if(!username || username.trim() === "" || username.length < config.minCharactersUsername || username.length > config.maxCharactersUsername) {
|
|
916
|
+
throw "BAD_USERNAME";
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if(usernameBanned(username)) {
|
|
920
|
+
throw "BANNED_USERNAME";
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if(await usernameAlreadyInUse(username)) {
|
|
924
|
+
throw "USERNAME_ALREADY_IN_USE";
|
|
925
|
+
}
|
|
869
926
|
}
|
|
870
927
|
|
|
871
928
|
app.engine("html", ejs.renderFile);
|
|
@@ -878,6 +935,44 @@ app.use(express.urlencoded({ extended: true }));
|
|
|
878
935
|
app.use(cookieParser());
|
|
879
936
|
app.use(i18n.init);
|
|
880
937
|
|
|
938
|
+
const csrfSecretAdmin = generateRandomJsonWebTokenSecretKey(jsonWebTokenSecretKeyAdmin);
|
|
939
|
+
const { doubleCsrfProtection: doubleCsrfProtectionAdmin, generateCsrfToken: generateCsrfTokenAdmin } = doubleCsrf({
|
|
940
|
+
getSecret: () => csrfSecretAdmin,
|
|
941
|
+
getSessionIdentifier: (req) => req.cookies.tokenAdmin || randomUUID(),
|
|
942
|
+
getCsrfTokenFromRequest: (req) => {
|
|
943
|
+
return (
|
|
944
|
+
req.headers["x-csrf-token"] ||
|
|
945
|
+
req.body?._csrf ||
|
|
946
|
+
req.query?._csrf
|
|
947
|
+
);
|
|
948
|
+
},
|
|
949
|
+
cookieName: productionMode ? "__Host-snakeia-server.x-csrf-token-admin" : "snakeia-server.x-csrf-token-admin",
|
|
950
|
+
cookieOptions: {
|
|
951
|
+
sameSite: productionMode ? "strict" : "lax",
|
|
952
|
+
path: "/",
|
|
953
|
+
secure: productionMode
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const csrfSecretUser = generateRandomJsonWebTokenSecretKey(jsonWebTokenSecretKeyAdmin);
|
|
958
|
+
const { doubleCsrfProtection: doubleCsrfProtectionUserAuthent, generateCsrfToken: generateCsrfTokenUserAuthent } = doubleCsrf({
|
|
959
|
+
getSecret: () => csrfSecretUser,
|
|
960
|
+
getSessionIdentifier: (req) => req.cookies.sessionId || randomUUID(),
|
|
961
|
+
getCsrfTokenFromRequest: (req) => {
|
|
962
|
+
return (
|
|
963
|
+
req.headers["x-csrf-token"] ||
|
|
964
|
+
req.body?._csrf ||
|
|
965
|
+
req.query?._csrf
|
|
966
|
+
);
|
|
967
|
+
},
|
|
968
|
+
cookieName: "snakeia-server.x-csrf-token-user",
|
|
969
|
+
cookieOptions: {
|
|
970
|
+
sameSite: "lax",
|
|
971
|
+
path: "/authentication",
|
|
972
|
+
secure: productionMode
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
881
976
|
// Rate limiter
|
|
882
977
|
app.use("/authentication", rateLimit({
|
|
883
978
|
windowMs: config.authentWindowMs,
|
|
@@ -887,15 +982,16 @@ app.use("/authentication", rateLimit({
|
|
|
887
982
|
|
|
888
983
|
// IP ban
|
|
889
984
|
app.use(function(req, res, next) {
|
|
890
|
-
ipBanned(req.ip)
|
|
891
|
-
res.
|
|
985
|
+
if(ipBanned(req.ip)) {
|
|
986
|
+
res.status(403);
|
|
987
|
+
|
|
988
|
+
return res.render(__dirname + "/views/banned.html", {
|
|
892
989
|
contact: config.contactBan,
|
|
893
990
|
theme: req.query.theme
|
|
894
991
|
});
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
next();
|
|
899
995
|
});
|
|
900
996
|
|
|
901
997
|
app.get("/", function(req, res) {
|
|
@@ -906,96 +1002,170 @@ app.get("/", function(req, res) {
|
|
|
906
1002
|
});
|
|
907
1003
|
});
|
|
908
1004
|
|
|
909
|
-
app.get("/authentication",
|
|
910
|
-
if(req.cookies
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1005
|
+
app.get("/authentication", async (req, res) => {
|
|
1006
|
+
if(!req.cookies || !config.enableAuthentication) {
|
|
1007
|
+
res.status(400);
|
|
1008
|
+
return res.end();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const authenticated = await checkAuthenticationExpress(req).then(() => true).catch(() => false);
|
|
1012
|
+
|
|
1013
|
+
setSessionCookie(req, res);
|
|
1014
|
+
|
|
1015
|
+
const clientCompatible = isClientCompatible(req.query.version, req.query.id);
|
|
1016
|
+
|
|
1017
|
+
if(!clientCompatible) {
|
|
1018
|
+
res.status(403);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
res.render(__dirname + "/views/authentication.html", {
|
|
1022
|
+
publicKey: config.recaptchaPublicKey,
|
|
1023
|
+
enableRecaptcha: config.enableRecaptcha,
|
|
1024
|
+
errorRecaptcha: false,
|
|
1025
|
+
errorUsername: false,
|
|
1026
|
+
errorUsernameBanned: false,
|
|
1027
|
+
errorUsernameAlreadyInUse: false,
|
|
1028
|
+
success: false,
|
|
1029
|
+
authent: authenticated,
|
|
1030
|
+
locale: i18n.getLocale(req),
|
|
1031
|
+
min: config.minCharactersUsername,
|
|
1032
|
+
max: config.maxCharactersUsername,
|
|
1033
|
+
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1034
|
+
maxTimeGame: config.maxTimeGame,
|
|
1035
|
+
theme: req.query.theme,
|
|
1036
|
+
clientCompatible,
|
|
1037
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1038
|
+
csrfToken: generateCsrfTokenUserAuthent(req, res, { overwrite: true, validateOnReuse: true }),
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
if(authenticated && clientCompatible) {
|
|
1042
|
+
sendTokenToSocket(req, getExpressUserToken(req));
|
|
933
1043
|
}
|
|
934
1044
|
});
|
|
935
1045
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
let err = false;
|
|
939
|
-
|
|
940
|
-
checkAuthenticationExpress(req).catch(() => err = true).finally(() => {
|
|
941
|
-
if(err) {
|
|
942
|
-
verifyFormAuthentication(req.body).then(() => {
|
|
943
|
-
const username = req.body["username"];
|
|
944
|
-
const id = req.query.id;
|
|
945
|
-
|
|
946
|
-
const token = jwt.sign({
|
|
947
|
-
username: username
|
|
948
|
-
}, jsonWebTokenSecretKey, { expiresIn: config.authenticationTime / 1000 });
|
|
949
|
-
|
|
950
|
-
res.cookie("token", token, { expires: new Date(Date.now() + config.authenticationTime), httpOnly: true, sameSite: "None", secure: (req.protocol == "https" ? true : false) });
|
|
951
|
-
|
|
952
|
-
res.render(__dirname + "/views/authentication.html", {
|
|
953
|
-
publicKey: config.recaptchaPublicKey,
|
|
954
|
-
enableRecaptcha: config.enableRecaptcha,
|
|
955
|
-
errorRecaptcha: false,
|
|
956
|
-
errorUsername: false,
|
|
957
|
-
errorUsernameBanned: false,
|
|
958
|
-
errorUsernameAlreadyInUse: false,
|
|
959
|
-
success: true,
|
|
960
|
-
authent: false,
|
|
961
|
-
locale: i18n.getLocale(req),
|
|
962
|
-
min: config.minCharactersUsername,
|
|
963
|
-
max: config.maxCharactersUsername,
|
|
964
|
-
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
965
|
-
maxTimeGame: config.maxTimeGame,
|
|
966
|
-
theme: req.query.theme
|
|
967
|
-
});
|
|
1046
|
+
function isClientCompatible(version, hasIdQueryParam) {
|
|
1047
|
+
const hasNoVersion = !version || version.trim().length === 0;
|
|
968
1048
|
|
|
969
|
-
|
|
1049
|
+
if((hasNoVersion && hasIdQueryParam) || hasNoVersion) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
970
1052
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
locale: i18n.getLocale(req),
|
|
985
|
-
min: config.minCharactersUsername,
|
|
986
|
-
max: config.maxCharactersUsername,
|
|
987
|
-
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
988
|
-
maxTimeGame: config.maxTimeGame,
|
|
989
|
-
theme: req.query.theme
|
|
990
|
-
});
|
|
991
|
-
});
|
|
992
|
-
}
|
|
1053
|
+
return semver.gte(version, GameConstants.Setting.APP_VERSION);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function setSessionCookie(req, res) {
|
|
1057
|
+
let sessionId = req.cookies.sessionId;
|
|
1058
|
+
|
|
1059
|
+
if(!sessionId) {
|
|
1060
|
+
sessionId = randomUUID();
|
|
1061
|
+
|
|
1062
|
+
res.cookie("sessionId", sessionId, {
|
|
1063
|
+
httpOnly: true,
|
|
1064
|
+
sameSite: "lax",
|
|
1065
|
+
secure: req.protocol === "https"
|
|
993
1066
|
});
|
|
994
|
-
|
|
995
|
-
|
|
1067
|
+
|
|
1068
|
+
req.cookies.sessionId = sessionId;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
app.post("/authentication", doubleCsrfProtectionUserAuthent, async (req, res) => {
|
|
1073
|
+
if(!req.cookies || !config.enableAuthentication) {
|
|
1074
|
+
res.status(400);
|
|
1075
|
+
return res.end();
|
|
996
1076
|
}
|
|
1077
|
+
|
|
1078
|
+
const alreadyAuthenticated = await checkAuthenticationExpress(req).then(() => true).catch(() => false);
|
|
1079
|
+
|
|
1080
|
+
if(alreadyAuthenticated) {
|
|
1081
|
+
return res.end();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const clientCompatible = isClientCompatible(req.query.version, req.query.id);
|
|
1085
|
+
|
|
1086
|
+
let formError = null;
|
|
1087
|
+
|
|
1088
|
+
if(clientCompatible) {
|
|
1089
|
+
await verifyFormAuthentication(req.body).catch(e => formError = e);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if(formError || !clientCompatible) {
|
|
1093
|
+
res.status(403);
|
|
1094
|
+
|
|
1095
|
+
return res.render(__dirname + "/views/authentication.html", {
|
|
1096
|
+
publicKey: config.recaptchaPublicKey,
|
|
1097
|
+
enableRecaptcha: config.enableRecaptcha,
|
|
1098
|
+
errorRecaptcha: formError == "INVALID_RECAPTCHA",
|
|
1099
|
+
errorUsername: formError == "BAD_USERNAME",
|
|
1100
|
+
errorUsernameBanned: formError == "BANNED_USERNAME",
|
|
1101
|
+
errorUsernameAlreadyInUse: formError == "USERNAME_ALREADY_IN_USE",
|
|
1102
|
+
success: false,
|
|
1103
|
+
authent: false,
|
|
1104
|
+
locale: i18n.getLocale(req),
|
|
1105
|
+
min: config.minCharactersUsername,
|
|
1106
|
+
max: config.maxCharactersUsername,
|
|
1107
|
+
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1108
|
+
maxTimeGame: config.maxTimeGame,
|
|
1109
|
+
theme: req.query.theme,
|
|
1110
|
+
clientCompatible,
|
|
1111
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1112
|
+
csrfToken: generateCsrfTokenUserAuthent(req, res, { overwrite: true, validateOnReuse: true })
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const username = req.body["username"];
|
|
1117
|
+
|
|
1118
|
+
const token = await new SignJWT({ username })
|
|
1119
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
1120
|
+
.setExpirationTime(Math.floor(config.authenticationTime / 1000) + "s")
|
|
1121
|
+
.setIssuedAt()
|
|
1122
|
+
.sign(jsonWebTokenSecretKey);
|
|
1123
|
+
|
|
1124
|
+
generateTokenCookie(res, token, req);
|
|
1125
|
+
|
|
1126
|
+
res.render(__dirname + "/views/authentication.html", {
|
|
1127
|
+
publicKey: config.recaptchaPublicKey,
|
|
1128
|
+
enableRecaptcha: config.enableRecaptcha,
|
|
1129
|
+
errorRecaptcha: false,
|
|
1130
|
+
errorUsername: false,
|
|
1131
|
+
errorUsernameBanned: false,
|
|
1132
|
+
errorUsernameAlreadyInUse: false,
|
|
1133
|
+
success: true,
|
|
1134
|
+
authent: false,
|
|
1135
|
+
locale: i18n.getLocale(req),
|
|
1136
|
+
min: config.minCharactersUsername,
|
|
1137
|
+
max: config.maxCharactersUsername,
|
|
1138
|
+
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1139
|
+
maxTimeGame: config.maxTimeGame,
|
|
1140
|
+
theme: req.query.theme,
|
|
1141
|
+
clientCompatible,
|
|
1142
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1143
|
+
csrfToken: null
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
logger.info("authentication - username: " + username + " - ip: " + req.ip);
|
|
1147
|
+
|
|
1148
|
+
sendTokenToSocket(req, token);
|
|
997
1149
|
});
|
|
998
1150
|
|
|
1151
|
+
function generateTokenCookie(res, token, req) {
|
|
1152
|
+
res.cookie("token", token, {
|
|
1153
|
+
expires: new Date(Date.now() + config.authenticationTime),
|
|
1154
|
+
httpOnly: true,
|
|
1155
|
+
sameSite: "None",
|
|
1156
|
+
secure: req.protocol === "https"
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function sendTokenToSocket(req, token) {
|
|
1161
|
+
const sessionId = req.cookies.sessionId;
|
|
1162
|
+
const socketId = socketSessions.get(sessionId);
|
|
1163
|
+
|
|
1164
|
+
if(socketId != null) {
|
|
1165
|
+
io.to("" + socketId).emit("token", token);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
999
1169
|
app.get("/rooms", function(req, res) {
|
|
1000
1170
|
const callbackName = req.query.callback;
|
|
1001
1171
|
res.charset = "UTF-8";
|
|
@@ -1019,25 +1189,18 @@ function kickUser(socketId, token) {
|
|
|
1019
1189
|
}
|
|
1020
1190
|
|
|
1021
1191
|
function kickUsername(username) {
|
|
1022
|
-
tokens.
|
|
1023
|
-
jwt.verify(token, jsonWebTokenSecretKey, function(err, data) {
|
|
1024
|
-
if(!err && data && data.username && data.username == username) {
|
|
1025
|
-
invalidateUserToken(token);
|
|
1026
|
-
}
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1192
|
+
const usernameToken = tokens.get(username.toLowerCase());
|
|
1030
1193
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
for(let i = tokens.length - 1; i >= 0; i--) {
|
|
1035
|
-
if(tokens[i] == token) {
|
|
1036
|
-
tokens.splice(i, 1);
|
|
1037
|
-
}
|
|
1194
|
+
if(usernameToken) {
|
|
1195
|
+
invalidateUserToken(username, usernameToken);
|
|
1038
1196
|
}
|
|
1039
1197
|
}
|
|
1040
1198
|
|
|
1199
|
+
function invalidateUserToken(username, token) {
|
|
1200
|
+
invalidatedUserTokens.add(token);
|
|
1201
|
+
tokens.delete(username.toLowerCase());
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1041
1204
|
function banUserIP(socketId) {
|
|
1042
1205
|
const sockets = io.of("/").sockets;
|
|
1043
1206
|
|
|
@@ -1057,17 +1220,16 @@ function banUserIP(socketId) {
|
|
|
1057
1220
|
}
|
|
1058
1221
|
}
|
|
1059
1222
|
|
|
1060
|
-
function banUserName(token) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1223
|
+
async function banUserName(token) {
|
|
1224
|
+
try {
|
|
1225
|
+
const { payload } = await jwtVerify(token, jsonWebTokenSecretKey);
|
|
1226
|
+
|
|
1227
|
+
if(payload.username) {
|
|
1228
|
+
config.usernameBan.push(payload.username);
|
|
1229
|
+
logger.info("username banned (" + payload.username + ")");
|
|
1068
1230
|
updateConfigToFile();
|
|
1069
1231
|
}
|
|
1070
|
-
}
|
|
1232
|
+
} catch {}
|
|
1071
1233
|
}
|
|
1072
1234
|
|
|
1073
1235
|
function unbanUsername(value) {
|
|
@@ -1095,13 +1257,21 @@ function manualIPBan(value) {
|
|
|
1095
1257
|
}
|
|
1096
1258
|
|
|
1097
1259
|
function resetLog() {
|
|
1098
|
-
|
|
1099
|
-
|
|
1260
|
+
try {
|
|
1261
|
+
fs.writeFileSync(config.logFile, "", "UTF-8");
|
|
1262
|
+
logger.info("log file reseted");
|
|
1263
|
+
} catch(e) {
|
|
1264
|
+
logger.error("failed to reset log file", e);
|
|
1265
|
+
}
|
|
1100
1266
|
}
|
|
1101
1267
|
|
|
1102
1268
|
function resetErrorLog() {
|
|
1103
|
-
|
|
1104
|
-
|
|
1269
|
+
try {
|
|
1270
|
+
fs.writeFileSync(config.errorLogFile, "", "UTF-8");
|
|
1271
|
+
logger.info("error log file reseted");
|
|
1272
|
+
} catch(e) {
|
|
1273
|
+
logger.error("failed to reset error log file", e);
|
|
1274
|
+
}
|
|
1105
1275
|
}
|
|
1106
1276
|
|
|
1107
1277
|
function updateConfig(value) {
|
|
@@ -1120,178 +1290,188 @@ function updateConfig(value) {
|
|
|
1120
1290
|
}
|
|
1121
1291
|
}
|
|
1122
1292
|
|
|
1123
|
-
function verifyFormAuthenticationAdmin(body) {
|
|
1124
|
-
|
|
1125
|
-
verifyRecaptcha(body["g-recaptcha-response"])
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const accounts = config.adminAccounts;
|
|
1293
|
+
async function verifyFormAuthenticationAdmin(body) {
|
|
1294
|
+
try {
|
|
1295
|
+
await verifyRecaptcha(body["g-recaptcha-response"]);
|
|
1296
|
+
} catch {
|
|
1297
|
+
throw "INVALID_RECAPTCHA";
|
|
1298
|
+
}
|
|
1130
1299
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1300
|
+
const { username, password } = body;
|
|
1301
|
+
const accounts = config.adminAccounts;
|
|
1133
1302
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1303
|
+
if(!accounts || !Object.keys(accounts).includes(username)) {
|
|
1304
|
+
throw "INVALID";
|
|
1305
|
+
}
|
|
1137
1306
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
reject("INVALID");
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}, () => {
|
|
1148
|
-
reject("INVALID_RECAPTCHA");
|
|
1149
|
-
});
|
|
1150
|
-
});
|
|
1307
|
+
const hashPassword = accounts[username]["password"];
|
|
1308
|
+
const enteredPasswordHash = createHash("sha512").update(password).digest("hex");
|
|
1309
|
+
|
|
1310
|
+
if(hashPassword !== enteredPasswordHash) {
|
|
1311
|
+
throw "INVALID";
|
|
1312
|
+
}
|
|
1151
1313
|
}
|
|
1152
1314
|
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
return (
|
|
1159
|
-
req.headers["x-csrf-token"] ||
|
|
1160
|
-
req.body?._csrf ||
|
|
1161
|
-
req.query?._csrf
|
|
1162
|
-
);
|
|
1163
|
-
},
|
|
1164
|
-
cookieName: productionMode ? "__Host-snakeia-server.x-csrf-token" : "snakeia-server.x-csrf-token",
|
|
1165
|
-
cookieOptions: {
|
|
1166
|
-
sameSite: productionMode ? "strict" : "lax",
|
|
1167
|
-
path: "/",
|
|
1168
|
-
secure: productionMode
|
|
1315
|
+
async function verifyAdminToken(req) {
|
|
1316
|
+
const token = req.cookies?.tokenAdmin;
|
|
1317
|
+
|
|
1318
|
+
if(!token || invalidatedAdminTokens.has(token)) {
|
|
1319
|
+
return null;
|
|
1169
1320
|
}
|
|
1321
|
+
|
|
1322
|
+
try {
|
|
1323
|
+
const { payload } = await jwtVerify(token, jsonWebTokenSecretKeyAdmin);
|
|
1324
|
+
const usernames = Object.keys(config.adminAccounts);
|
|
1325
|
+
|
|
1326
|
+
if(payload.username && usernames.includes(payload.username)) {
|
|
1327
|
+
return payload;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return null;
|
|
1331
|
+
} catch {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const adminAuthentRateLimiter = rateLimit({
|
|
1337
|
+
windowMs: config.adminAuthentWindowMs,
|
|
1338
|
+
max: config.adminAuthentMaxRequest,
|
|
1339
|
+
validate: { trustProxy: false }
|
|
1170
1340
|
});
|
|
1171
1341
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const usernames = Object.keys(config.adminAccounts);
|
|
1178
|
-
const authenticated = !err && data && data.username && usernames.includes(data.username);
|
|
1179
|
-
let role = "none";
|
|
1342
|
+
const adminActionsRateLimiter = rateLimit({
|
|
1343
|
+
windowMs: config.adminActionsWindowMs,
|
|
1344
|
+
max: config.adminActionsMaxRequest,
|
|
1345
|
+
validate: { trustProxy: false }
|
|
1346
|
+
});
|
|
1180
1347
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
io: io,
|
|
1199
|
-
config: config,
|
|
1200
|
-
csrfToken: generateCsrfToken(req, res, { overwrite: true, validateOnReuse: true }),
|
|
1201
|
-
serverLog: logFile,
|
|
1202
|
-
errorLog: errorLogFile,
|
|
1203
|
-
getIPSocketIO: getIPSocketIO,
|
|
1204
|
-
theme: req.query.theme
|
|
1205
|
-
});
|
|
1206
|
-
});
|
|
1207
|
-
});
|
|
1208
|
-
});
|
|
1209
|
-
} else {
|
|
1210
|
-
res.end();
|
|
1348
|
+
app.get("/admin", adminActionsRateLimiter, doubleCsrfProtectionAdmin, async (req, res) => {
|
|
1349
|
+
if(!req.cookies) {
|
|
1350
|
+
res.status(400);
|
|
1351
|
+
return res.end();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const payload = await verifyAdminToken(req);
|
|
1355
|
+
const authenticated = payload != null;
|
|
1356
|
+
const role = authenticated ? (config.adminAccounts[payload.username]["role"] || "moderator") : "none";
|
|
1357
|
+
|
|
1358
|
+
const [logFile, errorLogFile] = await Promise.all([
|
|
1359
|
+
fs.promises.readFile(config.logFile, "UTF-8").catch(() => ""),
|
|
1360
|
+
fs.promises.readFile(config.errorLogFile, "UTF-8").catch(() => "")
|
|
1361
|
+
]);
|
|
1362
|
+
|
|
1363
|
+
if(!authenticated) {
|
|
1364
|
+
res.status(401);
|
|
1211
1365
|
}
|
|
1366
|
+
|
|
1367
|
+
res.render(__dirname + "/views/admin.html", {
|
|
1368
|
+
publicKey: config.recaptchaPublicKey,
|
|
1369
|
+
enableRecaptcha: config.enableRecaptcha,
|
|
1370
|
+
authent: authenticated,
|
|
1371
|
+
role: role,
|
|
1372
|
+
username: authenticated ? payload.username : "",
|
|
1373
|
+
success: false,
|
|
1374
|
+
errorAuthent: false,
|
|
1375
|
+
errorRecaptcha: false,
|
|
1376
|
+
locale: i18n.getLocale(req),
|
|
1377
|
+
games: games,
|
|
1378
|
+
io: io,
|
|
1379
|
+
config: role === "administrator" ? config : null,
|
|
1380
|
+
csrfToken: generateCsrfTokenAdmin(req, res, { overwrite: true, validateOnReuse: true }),
|
|
1381
|
+
serverLog: logFile,
|
|
1382
|
+
errorLog: errorLogFile,
|
|
1383
|
+
getIPSocketIO: getIPSocketIO,
|
|
1384
|
+
theme: req.query.theme
|
|
1385
|
+
});
|
|
1212
1386
|
});
|
|
1213
1387
|
|
|
1214
|
-
function adminAction(req, res, action) {
|
|
1215
|
-
if(req.cookies) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
const usernames = Object.keys(config.adminAccounts);
|
|
1220
|
-
const authenticated = !err && data && data.username && usernames.includes(data.username);
|
|
1221
|
-
|
|
1222
|
-
if(authenticated) {
|
|
1223
|
-
const username = data.username;
|
|
1224
|
-
const role = config.adminAccounts[username]["role"] || "moderator";
|
|
1225
|
-
|
|
1226
|
-
if(action == "disconnect") {
|
|
1227
|
-
invalidatedAdminTokens.push(req.cookies.tokenAdmin);
|
|
1228
|
-
res.cookie("tokenAdmin", { expires: -1 });
|
|
1229
|
-
res.redirect("/admin");
|
|
1230
|
-
return;
|
|
1231
|
-
} else if(action) {
|
|
1232
|
-
const socket = req.body.socket;
|
|
1233
|
-
const token = req.body.token;
|
|
1234
|
-
const value = req.body.value;
|
|
1235
|
-
|
|
1236
|
-
switch(action) {
|
|
1237
|
-
case "kick":
|
|
1238
|
-
kickUser(socket, token);
|
|
1239
|
-
break;
|
|
1240
|
-
case "banIP":
|
|
1241
|
-
if(value) {
|
|
1242
|
-
manualIPBan(value);
|
|
1243
|
-
} else {
|
|
1244
|
-
banUserIP(socket);
|
|
1245
|
-
kickUser(socket, token);
|
|
1246
|
-
}
|
|
1247
|
-
break;
|
|
1248
|
-
case "banUserName":
|
|
1249
|
-
if(value) {
|
|
1250
|
-
manualUsernameBan(value);
|
|
1251
|
-
kickUsername(value);
|
|
1252
|
-
} else {
|
|
1253
|
-
banUserName(token);
|
|
1254
|
-
kickUser(socket, token);
|
|
1255
|
-
}
|
|
1256
|
-
break;
|
|
1257
|
-
case "banIPUserName":
|
|
1258
|
-
banUserIP(socket);
|
|
1259
|
-
banUserName(token);
|
|
1260
|
-
kickUser(socket, token);
|
|
1261
|
-
break;
|
|
1262
|
-
case "unbanUsername":
|
|
1263
|
-
unbanUsername(value);
|
|
1264
|
-
break;
|
|
1265
|
-
case "unbanIP":
|
|
1266
|
-
unbanIP(value);
|
|
1267
|
-
break;
|
|
1268
|
-
case "resetLog":
|
|
1269
|
-
if(role === "administrator") resetLog();
|
|
1270
|
-
break;
|
|
1271
|
-
case "resetErrorLog":
|
|
1272
|
-
if(role === "administrator") resetErrorLog();
|
|
1273
|
-
break;
|
|
1274
|
-
case "updateConfig":
|
|
1275
|
-
if(role === "administrator") updateConfig(value);
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1388
|
+
async function adminAction(req, res, action) {
|
|
1389
|
+
if(!req.cookies) {
|
|
1390
|
+
res.status(400);
|
|
1391
|
+
return res.end();
|
|
1392
|
+
}
|
|
1278
1393
|
|
|
1279
|
-
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1394
|
+
const payload = await verifyAdminToken(req);
|
|
1283
1395
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1396
|
+
if(!payload) {
|
|
1397
|
+
return res.redirect(303, "/admin");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const role = config.adminAccounts[payload.username]["role"] || "moderator";
|
|
1401
|
+
|
|
1402
|
+
if(action === "disconnect") {
|
|
1403
|
+
invalidatedAdminTokens.add(req.cookies.tokenAdmin);
|
|
1404
|
+
res.clearCookie("tokenAdmin");
|
|
1405
|
+
|
|
1406
|
+
return res.redirect(303, "/admin");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const { socket, token, value } = req.body;
|
|
1410
|
+
|
|
1411
|
+
switch(action) {
|
|
1412
|
+
case "kick":
|
|
1413
|
+
kickUser(socket, token);
|
|
1414
|
+
break;
|
|
1415
|
+
case "banIP":
|
|
1416
|
+
if(value) {
|
|
1417
|
+
manualIPBan(value);
|
|
1418
|
+
} else {
|
|
1419
|
+
banUserIP(socket);
|
|
1420
|
+
kickUser(socket, token);
|
|
1421
|
+
}
|
|
1422
|
+
break;
|
|
1423
|
+
case "banUserName":
|
|
1424
|
+
if(value) {
|
|
1425
|
+
manualUsernameBan(value);
|
|
1426
|
+
kickUsername(value);
|
|
1427
|
+
} else {
|
|
1428
|
+
await banUserName(token);
|
|
1429
|
+
kickUser(socket, token);
|
|
1430
|
+
}
|
|
1431
|
+
break;
|
|
1432
|
+
case "banIPUserName":
|
|
1433
|
+
banUserIP(socket);
|
|
1434
|
+
await banUserName(token);
|
|
1435
|
+
kickUser(socket, token);
|
|
1436
|
+
break;
|
|
1437
|
+
case "unbanUsername":
|
|
1438
|
+
unbanUsername(value);
|
|
1439
|
+
break;
|
|
1440
|
+
case "unbanIP":
|
|
1441
|
+
unbanIP(value);
|
|
1442
|
+
break;
|
|
1443
|
+
case "resetLog":
|
|
1444
|
+
if(role === "administrator") {
|
|
1445
|
+
resetLog();
|
|
1446
|
+
} else {
|
|
1447
|
+
res.status(403);
|
|
1448
|
+
return res.end();
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
case "resetErrorLog":
|
|
1452
|
+
if(role === "administrator") {
|
|
1453
|
+
resetErrorLog();
|
|
1454
|
+
} else {
|
|
1455
|
+
res.status(403);
|
|
1456
|
+
return res.end();
|
|
1457
|
+
}
|
|
1458
|
+
break;
|
|
1459
|
+
case "updateConfig":
|
|
1460
|
+
if(role === "administrator") {
|
|
1461
|
+
updateConfig(value);
|
|
1462
|
+
} else {
|
|
1463
|
+
res.status(403);
|
|
1464
|
+
return res.end();
|
|
1465
|
+
}
|
|
1466
|
+
break;
|
|
1289
1467
|
}
|
|
1468
|
+
|
|
1469
|
+
res.redirect(303, "/admin");
|
|
1290
1470
|
}
|
|
1291
1471
|
|
|
1292
1472
|
const jsonParser = bodyParser.json();
|
|
1293
1473
|
|
|
1294
|
-
app.post("/admin/:action", jsonParser,
|
|
1474
|
+
app.post("/admin/:action", adminActionsRateLimiter, jsonParser, doubleCsrfProtectionAdmin, function(req, res) {
|
|
1295
1475
|
adminAction(req, res, req.params.action);
|
|
1296
1476
|
});
|
|
1297
1477
|
|
|
@@ -1301,113 +1481,123 @@ app.use(function (err, req, res, next) {
|
|
|
1301
1481
|
res.send("Error: invalid CSRF token");
|
|
1302
1482
|
});
|
|
1303
1483
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1484
|
+
app.post("/admin", adminAuthentRateLimiter, async (req, res) => {
|
|
1485
|
+
if(!req.cookies) {
|
|
1486
|
+
res.status(400);
|
|
1487
|
+
return res.end();
|
|
1488
|
+
}
|
|
1309
1489
|
|
|
1310
|
-
|
|
1311
|
-
if(req.cookies) {
|
|
1312
|
-
jwt.verify(req.cookies.tokenAdmin, jsonWebTokenSecretKeyAdmin, function(err, data) {
|
|
1313
|
-
if(invalidatedAdminTokens.includes(req.cookies.tokenAdmin)) res = true;
|
|
1490
|
+
const payload = await verifyAdminToken(req);
|
|
1314
1491
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1492
|
+
if(payload) {
|
|
1493
|
+
return res.redirect(303, "/admin");
|
|
1494
|
+
}
|
|
1318
1495
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
serverLog: null,
|
|
1340
|
-
errorLog: null,
|
|
1341
|
-
getIPSocketIO: getIPSocketIO,
|
|
1342
|
-
theme: req.query.theme
|
|
1343
|
-
});
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1496
|
+
try {
|
|
1497
|
+
await verifyFormAuthenticationAdmin(req.body);
|
|
1498
|
+
} catch(err) {
|
|
1499
|
+
res.status(403);
|
|
1500
|
+
|
|
1501
|
+
return res.render(__dirname + "/views/admin.html", {
|
|
1502
|
+
publicKey: config.recaptchaPublicKey,
|
|
1503
|
+
enableRecaptcha: config.enableRecaptcha,
|
|
1504
|
+
authent: false,
|
|
1505
|
+
errorAuthent: true,
|
|
1506
|
+
errorRecaptcha: err == "INVALID_RECAPTCHA",
|
|
1507
|
+
locale: i18n.getLocale(req),
|
|
1508
|
+
games: null,
|
|
1509
|
+
io: null,
|
|
1510
|
+
config: null,
|
|
1511
|
+
csrfToken: null,
|
|
1512
|
+
serverLog: null,
|
|
1513
|
+
errorLog: null,
|
|
1514
|
+
getIPSocketIO: getIPSocketIO,
|
|
1515
|
+
theme: req.query.theme
|
|
1346
1516
|
});
|
|
1347
|
-
} else {
|
|
1348
|
-
res.end();
|
|
1349
1517
|
}
|
|
1518
|
+
|
|
1519
|
+
const username = req.body["username"];
|
|
1520
|
+
|
|
1521
|
+
const token = await new SignJWT({ username })
|
|
1522
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
1523
|
+
.setExpirationTime(Math.floor(config.authenticationTime / 1000) + "s")
|
|
1524
|
+
.setIssuedAt()
|
|
1525
|
+
.sign(jsonWebTokenSecretKeyAdmin);
|
|
1526
|
+
|
|
1527
|
+
res.cookie("tokenAdmin", token, {
|
|
1528
|
+
expires: new Date(Date.now() + config.authenticationTime),
|
|
1529
|
+
httpOnly: true,
|
|
1530
|
+
sameSite: "strict",
|
|
1531
|
+
secure: req.protocol === "https"
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
res.redirect(303, "/admin");
|
|
1535
|
+
|
|
1536
|
+
logger.info("admin authent - username: " + username + " - ip: " + req.ip);
|
|
1350
1537
|
});
|
|
1351
1538
|
|
|
1352
1539
|
io.use(ioCookieParser());
|
|
1353
1540
|
|
|
1354
1541
|
function getIPSocketIO(req) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
if(req && req.headers) {
|
|
1542
|
+
if(req?.headers) {
|
|
1358
1543
|
const forwardedIpsStr = req.headers["x-forwarded-for"];
|
|
1359
|
-
|
|
1360
|
-
if(forwardedIpsStr &&
|
|
1361
|
-
const forwardedIps = forwardedIpsStr.split(",");
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1544
|
+
|
|
1545
|
+
if(forwardedIpsStr && config.proxyMode) {
|
|
1546
|
+
const forwardedIps = forwardedIpsStr.split(",").map(ip => ip.trim());
|
|
1547
|
+
const index = forwardedIps.length - config.numberOfProxies;
|
|
1548
|
+
|
|
1549
|
+
if(index >= 0) {
|
|
1550
|
+
return forwardedIps[index];
|
|
1551
|
+
}
|
|
1367
1552
|
}
|
|
1368
|
-
}
|
|
1369
1553
|
|
|
1370
|
-
|
|
1554
|
+
return req.address;
|
|
1555
|
+
}
|
|
1371
1556
|
}
|
|
1372
1557
|
|
|
1373
|
-
function checkAuthentication(token) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
});
|
|
1558
|
+
async function checkAuthentication(token) {
|
|
1559
|
+
if(!config.enableAuthentication) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if(!token || invalidatedUserTokens.has(token)) {
|
|
1564
|
+
throw new Error("UNAUTHORIZED");
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
try {
|
|
1568
|
+
await jwtVerify(token, jsonWebTokenSecretKey);
|
|
1569
|
+
return token;
|
|
1570
|
+
} catch {
|
|
1571
|
+
throw new Error("UNAUTHORIZED");
|
|
1572
|
+
}
|
|
1389
1573
|
}
|
|
1390
1574
|
|
|
1391
1575
|
function checkAuthenticationSocket(socket) {
|
|
1392
|
-
return checkAuthentication(socket
|
|
1576
|
+
return checkAuthentication(Player.getSocketToken(socket));
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function getExpressUserToken(req) {
|
|
1580
|
+
return req.cookies.token;
|
|
1393
1581
|
}
|
|
1394
1582
|
|
|
1395
1583
|
function checkAuthenticationExpress(req) {
|
|
1396
|
-
return checkAuthentication(req
|
|
1584
|
+
return checkAuthentication(getExpressUserToken(req));
|
|
1397
1585
|
}
|
|
1398
1586
|
|
|
1399
1587
|
const checkBanned = function(socket, next) {
|
|
1400
|
-
ipBanned(getIPSocketIO(socket.handshake))
|
|
1401
|
-
next(new Error(GameConstants.Error.BANNED));
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1588
|
+
if(ipBanned(getIPSocketIO(socket.handshake))) {
|
|
1589
|
+
return next(new Error(GameConstants.Error.BANNED));
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
next();
|
|
1405
1593
|
};
|
|
1406
1594
|
|
|
1407
1595
|
io.use(checkBanned);
|
|
1408
1596
|
|
|
1409
|
-
io.of("/rooms").use(ioCookieParser()).use(checkBanned).on("connection",
|
|
1410
|
-
|
|
1597
|
+
io.of("/rooms").use(ioCookieParser()).use(checkBanned).on("connection", async (socket) => {
|
|
1598
|
+
try {
|
|
1599
|
+
await checkAuthenticationSocket(socket);
|
|
1600
|
+
|
|
1411
1601
|
socket.emit("rooms", {
|
|
1412
1602
|
rooms: getRoomsData(),
|
|
1413
1603
|
serverVersion: config.version,
|
|
@@ -1421,49 +1611,59 @@ io.of("/rooms").use(ioCookieParser()).use(checkBanned).on("connection", function
|
|
|
1421
1611
|
enableAI: config.enableAI
|
|
1422
1612
|
}
|
|
1423
1613
|
});
|
|
1424
|
-
}
|
|
1425
|
-
socket
|
|
1426
|
-
}
|
|
1614
|
+
} catch(e) {
|
|
1615
|
+
emitAuthenticationRequired(socket);
|
|
1616
|
+
}
|
|
1427
1617
|
});
|
|
1428
1618
|
|
|
1429
|
-
io.of("/createRoom").use(ioCookieParser()).use(checkBanned).on("connection",
|
|
1430
|
-
socket.on("create",
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1619
|
+
io.of("/createRoom").use(ioCookieParser()).use(checkBanned).on("connection", (socket) => {
|
|
1620
|
+
socket.on("create", async (data) => {
|
|
1621
|
+
try {
|
|
1622
|
+
await checkAuthenticationSocket(socket);
|
|
1623
|
+
await createRoom(data, socket);
|
|
1624
|
+
} catch {
|
|
1625
|
+
emitAuthenticationRequired(socket);
|
|
1626
|
+
}
|
|
1436
1627
|
});
|
|
1437
1628
|
});
|
|
1438
1629
|
|
|
1439
|
-
io.on("connection",
|
|
1440
|
-
|
|
1630
|
+
io.on("connection", async (socket) => {
|
|
1631
|
+
try {
|
|
1632
|
+
const token = await checkAuthenticationSocket(socket);
|
|
1633
|
+
const username = await Player.getUsernameToken(token, jsonWebTokenSecretKey);
|
|
1634
|
+
|
|
1635
|
+
if(!username) return;
|
|
1636
|
+
|
|
1441
1637
|
socket.emit("authent", GameConstants.GameState.AUTHENTICATION_SUCCESS);
|
|
1442
|
-
tokens.
|
|
1638
|
+
tokens.set(username.toLowerCase(), token);
|
|
1443
1639
|
|
|
1444
|
-
socket.on("join-room",
|
|
1445
|
-
const code = data
|
|
1446
|
-
const version = data.version;
|
|
1640
|
+
socket.on("join-room", async (data) => {
|
|
1641
|
+
const { code, version } = data;
|
|
1447
1642
|
const game = games[code];
|
|
1448
|
-
|
|
1449
|
-
if(game != null
|
|
1643
|
+
|
|
1644
|
+
if(game != null
|
|
1645
|
+
&& !Player.containsId(game.players, socket.id)
|
|
1646
|
+
&& !Player.containsId(game.spectators, socket.id)
|
|
1647
|
+
&& !Player.containsToken(game.players, token)
|
|
1648
|
+
&& !Player.containsToken(game.spectators, token)
|
|
1649
|
+
&& !Player.containsTokenAllGames(token, games)
|
|
1650
|
+
&& !Player.containsIdAllGames(socket.id, games)
|
|
1651
|
+
) {
|
|
1450
1652
|
socket.join("room-" + code);
|
|
1451
|
-
|
|
1653
|
+
|
|
1452
1654
|
if(game.players.length + game.numberAIToAdd >= getMaxPlayers(code) || game.started) {
|
|
1453
|
-
game.spectators.push(new Player(token, socket.id, null, false, version));
|
|
1655
|
+
game.spectators.push(new Player(token, username, socket.id, null, false, version));
|
|
1454
1656
|
} else {
|
|
1455
|
-
game.players.push(new Player(token, socket.id, null, false, version));
|
|
1657
|
+
game.players.push(new Player(token, username, socket.id, null, false, version));
|
|
1456
1658
|
}
|
|
1457
|
-
|
|
1458
|
-
socket.emit("join-room", {
|
|
1459
|
-
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1659
|
+
|
|
1660
|
+
socket.emit("join-room", { success: true });
|
|
1661
|
+
|
|
1462
1662
|
socket.once("start", () => {
|
|
1463
1663
|
if(Player.containsId(game.players, socket.id)) {
|
|
1464
1664
|
Player.getPlayer(game.players, socket.id).ready = true;
|
|
1465
1665
|
}
|
|
1466
|
-
|
|
1666
|
+
|
|
1467
1667
|
if(game.started) {
|
|
1468
1668
|
socket.emit("init", {
|
|
1469
1669
|
"paused": game.gameEngine.paused,
|
|
@@ -1485,6 +1685,8 @@ io.on("connection", function(socket) {
|
|
|
1485
1685
|
"confirmExit": false,
|
|
1486
1686
|
"getInfos": false,
|
|
1487
1687
|
"getInfosGame": false,
|
|
1688
|
+
"getInfosControls": false,
|
|
1689
|
+
"getInfosGoal": false,
|
|
1488
1690
|
"errorOccurred": game.gameEngine.errorOccurred,
|
|
1489
1691
|
"timerToDisplay": config.enableMaxTimeGame ? (config.maxTimeGame - (Date.now() - game.timeStart)) / 1000 : -1,
|
|
1490
1692
|
"countBeforePlay": game.gameEngine.countBeforePlay,
|
|
@@ -1502,89 +1704,120 @@ io.on("connection", function(socket) {
|
|
|
1502
1704
|
"offsetFrame": game.gameEngine.speed * GameConstants.Setting.TIME_MULTIPLIER
|
|
1503
1705
|
});
|
|
1504
1706
|
}
|
|
1505
|
-
|
|
1707
|
+
|
|
1506
1708
|
gameMatchmaking(game, code);
|
|
1507
|
-
|
|
1709
|
+
|
|
1508
1710
|
socket.on("start", () => {
|
|
1509
|
-
socket.emit("start", {
|
|
1510
|
-
"paused": false
|
|
1511
|
-
});
|
|
1711
|
+
socket.emit("start", { "paused": false });
|
|
1512
1712
|
});
|
|
1513
1713
|
});
|
|
1514
|
-
|
|
1515
|
-
socket.once("exit", () =>
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
socket.on("key", function(key) {
|
|
1714
|
+
|
|
1715
|
+
socket.once("exit", () => exitGame(game, socket, code));
|
|
1716
|
+
socket.once("kill", () => exitGame(game, socket, code));
|
|
1717
|
+
socket.once("error", () => exitGame(game, socket, code));
|
|
1718
|
+
socket.once("disconnect", () => exitGame(game, socket, code));
|
|
1719
|
+
|
|
1720
|
+
socket.on("key", (key) => {
|
|
1524
1721
|
if(game != null && Player.containsId(game.players, socket.id) && Player.getPlayer(game.players, socket.id).snake) {
|
|
1525
1722
|
Player.getPlayer(game.players, socket.id).snake.lastKey = key;
|
|
1526
1723
|
sendStatus(code);
|
|
1527
1724
|
}
|
|
1528
1725
|
});
|
|
1529
|
-
|
|
1530
|
-
socket.on("pause", () => {
|
|
1531
|
-
|
|
1532
|
-
"paused": true
|
|
1533
|
-
});
|
|
1534
|
-
});
|
|
1535
|
-
|
|
1726
|
+
|
|
1727
|
+
socket.on("pause", () => socket.emit("pause", { "paused": true }));
|
|
1728
|
+
|
|
1536
1729
|
socket.on("reset", () => {
|
|
1537
1730
|
if(!game.started) {
|
|
1538
1731
|
gameMatchmaking(game, code);
|
|
1539
|
-
|
|
1540
|
-
socket.emit("reset", {
|
|
1541
|
-
"gameOver": false,
|
|
1542
|
-
"gameFinished": false,
|
|
1543
|
-
"scoreMax": false,
|
|
1544
|
-
"gameMazeWin": false
|
|
1545
|
-
});
|
|
1732
|
+
socket.emit("reset", { "gameOver": false, "gameFinished": false, "scoreMax": false, "gameMazeWin": false });
|
|
1546
1733
|
}
|
|
1547
1734
|
});
|
|
1548
|
-
|
|
1549
|
-
socket.once("error", () => {
|
|
1550
|
-
exitGame(game, socket, code);
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
socket.once("disconnect", () => {
|
|
1554
|
-
exitGame(game, socket, code);
|
|
1555
|
-
});
|
|
1556
|
-
|
|
1735
|
+
|
|
1557
1736
|
socket.on("forceStart", () => {
|
|
1558
1737
|
if(game != null && Player.containsId(game.players, socket.id) && game.players[0].id == socket.id && !game.started) {
|
|
1559
1738
|
startGame(code);
|
|
1560
1739
|
}
|
|
1561
1740
|
});
|
|
1562
|
-
|
|
1563
|
-
logger.info("join room (code: " + code + ") - username: " + Player.getUsernameSocket(socket) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
|
|
1741
|
+
|
|
1742
|
+
logger.info("join room (code: " + code + ") - username: " + (await Player.getUsernameSocket(socket, jsonWebTokenSecretKey)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
|
|
1564
1743
|
} else {
|
|
1565
1744
|
if(games[code] == null) {
|
|
1566
|
-
socket.emit("join-room", {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
});
|
|
1745
|
+
socket.emit("join-room", { success: false, errorCode: GameConstants.Error.ROOM_NOT_FOUND });
|
|
1746
|
+
} else if(
|
|
1747
|
+
Player.containsId(game.players, socket.id)
|
|
1748
|
+
|| Player.containsId(game.spectators, socket.id)
|
|
1749
|
+
|| Player.containsToken(game.players, token)
|
|
1750
|
+
|| Player.containsToken(game.spectators, token)
|
|
1751
|
+
) {
|
|
1752
|
+
socket.emit("join-room", { success: false, errorCode: GameConstants.Error.ROOM_ALREADY_JOINED });
|
|
1575
1753
|
} else if(Player.containsTokenAllGames(token, games) || Player.containsIdAllGames(socket.id, games)) {
|
|
1576
|
-
socket.emit("join-room", {
|
|
1577
|
-
success: false,
|
|
1578
|
-
errorCode: GameConstants.Error.ALREADY_CREATED_ROOM
|
|
1579
|
-
});
|
|
1754
|
+
socket.emit("join-room", { success: false, errorCode: GameConstants.Error.ALREADY_CREATED_ROOM });
|
|
1580
1755
|
}
|
|
1581
1756
|
}
|
|
1582
1757
|
});
|
|
1583
|
-
}
|
|
1584
|
-
socket
|
|
1585
|
-
}
|
|
1758
|
+
} catch {
|
|
1759
|
+
emitAuthenticationRequired(socket);
|
|
1760
|
+
}
|
|
1586
1761
|
});
|
|
1587
1762
|
|
|
1763
|
+
function emitAuthenticationRequired(socket) {
|
|
1764
|
+
const sessionId = socket.request.cookies.sessionId;
|
|
1765
|
+
|
|
1766
|
+
if(sessionId) {
|
|
1767
|
+
socketSessions.set(sessionId, socket.id);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
socket.on("disconnect", () => {
|
|
1771
|
+
socketSessions.delete(sessionId);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
socket.emit("authent", GameConstants.Error.AUTHENTICATION_REQUIRED);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function isTokenExpired(token) {
|
|
1778
|
+
try {
|
|
1779
|
+
const decoded = decodeJwt(token);
|
|
1780
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1781
|
+
|
|
1782
|
+
return !decoded?.exp || decoded.exp < now;
|
|
1783
|
+
} catch {
|
|
1784
|
+
return true;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function cleanMap(map) {
|
|
1789
|
+
for(const [key, token] of map.entries()) {
|
|
1790
|
+
if(isTokenExpired(token)) {
|
|
1791
|
+
map.delete(key);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function cleanSet(set) {
|
|
1797
|
+
for(const token of set) {
|
|
1798
|
+
if(isTokenExpired(token)) {
|
|
1799
|
+
set.delete(token);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function cleanSocketSessions() {
|
|
1805
|
+
for(const [sessionId, socketId] of socketSessions.entries()) {
|
|
1806
|
+
if(!io.sockets.sockets.get(socketId)) {
|
|
1807
|
+
socketSessions.delete(sessionId);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function cleanupTokenMaps() {
|
|
1813
|
+
cleanMap(tokens);
|
|
1814
|
+
cleanSet(invalidatedUserTokens);
|
|
1815
|
+
cleanSet(invalidatedAdminTokens);
|
|
1816
|
+
cleanSocketSessions();
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1588
1819
|
server.listen(config.port, () => {
|
|
1589
1820
|
console.log("listening on *:" + config.port);
|
|
1590
|
-
});
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
setInterval(cleanupTokenMaps, 60 * 1000);
|