snakeia-server 2.0.0-beta.1 → 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/README.md +74 -8
- package/config/default.json +7 -2
- package/locales/en.json +2 -1
- package/locales/fr.json +2 -1
- package/package.json +6 -5
- package/server.js +123 -30
- package/views/admin.html +12 -2
- package/views/authentication.html +3 -1
package/.drone.yml
CHANGED
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A server for my [SnakeIA](https://github.com/Eliastik/snakeia) game, written in
|
|
|
8
8
|
|
|
9
9
|
## About this server
|
|
10
10
|
|
|
11
|
-
* Version 2.0.0
|
|
11
|
+
* Version 2.0.0 (05/08/2026)
|
|
12
12
|
* Made in France by Eliastik - [eliastiksofts.com](http://eliastiksofts.com) - Contact : [eliastiksofts.com/contact](http://eliastiksofts.com/contact)
|
|
13
13
|
* License: GNU GPLv3 (see LICENCE.txt file)
|
|
14
14
|
|
|
@@ -74,7 +74,7 @@ You can create another configuration file in the **config** directory named **lo
|
|
|
74
74
|
````
|
|
75
75
|
{
|
|
76
76
|
"ServerConfig": {
|
|
77
|
-
"version": "2.0.0
|
|
77
|
+
"version": "2.0.0", // The server version
|
|
78
78
|
"port": 3000, // The port where the server runs
|
|
79
79
|
"enableHttps": false, // Enable or disable HTTPS listening on the server. If disabled, the server will only listen on HTTP.
|
|
80
80
|
"httpsCertFile": "path/to/https/cert.pem", // Path to HTTPS certificate file
|
|
@@ -93,6 +93,7 @@ You can create another configuration file in the **config** directory named **lo
|
|
|
93
93
|
"aiUltraModelID": null, // ID of the model (as returned by the API at the URL above) to load for the Ultra AI. Can be left empty; in that case, the default model provided by the API will be loaded
|
|
94
94
|
"aiUltraCustomModelURL": null, // A URL pointing to a custom AI model to load. Must be a TensorFlow.js model trained for the Ultra AI. If there's an issue, game initialization will fail when Ultra AIs are present in the game
|
|
95
95
|
"playerWaitTime": 45000, // The time while waiting for players to join a room (ms)
|
|
96
|
+
"matchmakingWaitTime": 30000, // The amount of time you must wait before resuming matchmaking after a game ends (ms)
|
|
96
97
|
"enableMaxTimeGame": true, // Enable time limit for each game
|
|
97
98
|
"maxTimeGame": 300000, // The time limit for each game (ms)
|
|
98
99
|
"enableAuthentication": true, // Enable authentification when connecting to the server
|
|
@@ -105,8 +106,12 @@ You can create another configuration file in the **config** directory named **lo
|
|
|
105
106
|
"recaptchaApiUrl": "https://www.google.com/recaptcha/api/siteverify", // ReCaptcha API URL
|
|
106
107
|
"recaptchaPublicKey": "", // ReCaptcha public key (if not provided, the ReCaptcha will be disabled)
|
|
107
108
|
"recaptchaPrivateKey": "", // ReCaptcha private key (if not provided, the ReCaptcha will be disabled)
|
|
108
|
-
"authentMaxRequest":
|
|
109
|
-
"authentWindowMs": 900000, // Time
|
|
109
|
+
"authentMaxRequest": 15, // Maximum number of authentication attempts per time window
|
|
110
|
+
"authentWindowMs": 900000, // Time window duration (for authentication) in ms (900000 = 15 minutes)
|
|
111
|
+
"adminAuthentMaxRequest": 5, // Maximum number of authentication attempts on the admin panel per time window
|
|
112
|
+
"adminAuthentWindowMs": 900000, // Time window duration (for admin authentication) in ms (900000 = 15 minutes)
|
|
113
|
+
"adminActionsMaxRequest": 30, // Maximum number of actions on the admin panel per time window
|
|
114
|
+
"adminActionsWindowMs": 60000, // Time window duration (for admin actions) in ms (60000 = 1 minute)
|
|
110
115
|
"ipBan": [], // A list of IP to ban
|
|
111
116
|
"usernameBan": [], // A list of usernames to ban
|
|
112
117
|
"contactBan": "", // A contact URL displayed when an user is banned
|
|
@@ -126,6 +131,34 @@ You can create another configuration file in the **config** directory named **lo
|
|
|
126
131
|
|
|
127
132
|
## Changelog
|
|
128
133
|
|
|
134
|
+
* Version 2.0.0 (5/8/2026):
|
|
135
|
+
- ⚠️ Breaking Changes:
|
|
136
|
+
- Authentication is no longer compatible with SnakeIA 3.1.0 and earlier versions;
|
|
137
|
+
- Clients must be updated to SnakeIA 3.2.0 to support the new authentication method;
|
|
138
|
+
- An explicit error message is now displayed when an incompatible client attempts to authenticate;
|
|
139
|
+
- Updated to SnakeIA 3.2.0 to benefit from the latest improvements;
|
|
140
|
+
- Matchmaking now automatically restarts 30 seconds after the end of a game (configurable through the `matchmakingWaitTime` setting);
|
|
141
|
+
- Bug fixes and security improvements:
|
|
142
|
+
- Fixed a security issue in the previous authentication system:
|
|
143
|
+
- The socket ID was transmitted through the authentication URL;
|
|
144
|
+
- An attacker could reuse this URL to retrieve the victim's token;
|
|
145
|
+
- The impact remained limited since accounts are currently ephemeral;
|
|
146
|
+
- Improved security by implementing CSRF protection on most endpoints (previously, only a few endpoints were covered);
|
|
147
|
+
- Strengthened the rate limiting system:
|
|
148
|
+
- Stricter rate limiting rules;
|
|
149
|
+
- Added rate limiting for administration actions;
|
|
150
|
+
- Separate configurations for:
|
|
151
|
+
- User authentication;
|
|
152
|
+
- Administrator authentication;
|
|
153
|
+
- Administration actions;
|
|
154
|
+
- Fixed issues related to retrieving usernames from tokens;
|
|
155
|
+
- Fixed a server crash occurring when modifying the configuration from the administration/moderation panel while the configuration file was read-only;
|
|
156
|
+
- Technical improvements:
|
|
157
|
+
- Migrated to the Jose library for authentication token management;
|
|
158
|
+
- Improved HTTP responses to better comply with REST API standards across multiple endpoints;
|
|
159
|
+
- Codebase refactoring;
|
|
160
|
+
- Updated dependencies.
|
|
161
|
+
|
|
129
162
|
* Version 2.0.0-beta.1 (3/7/2026):
|
|
130
163
|
- ⚠️ Breaking Changes:
|
|
131
164
|
- Authentication is no longer compatible with SnakeIA 3.1.0 and earlier versions.
|
|
@@ -237,7 +270,7 @@ Un serveur pour mon jeu [SnakeIA](https://github.com/Eliastik/snakeia), écrit e
|
|
|
237
270
|
|
|
238
271
|
## À propos de ce serveur
|
|
239
272
|
|
|
240
|
-
* Version 2.0.0
|
|
273
|
+
* Version 2.0.0 (08/05/2026)
|
|
241
274
|
* Made in France by Eliastik - [eliastiksofts.com](http://eliastiksofts.com) - Contact : [eliastiksofts.com/contact](http://eliastiksofts.com/contact)
|
|
242
275
|
* Licence : GNU GPLv3 (voir le fichier LICENCE.txt)
|
|
243
276
|
|
|
@@ -303,7 +336,7 @@ Vous pouvez créer un fichier de configuration **local.json** dans le dossier **
|
|
|
303
336
|
````
|
|
304
337
|
{
|
|
305
338
|
"ServerConfig": {
|
|
306
|
-
"version": "2.0.0
|
|
339
|
+
"version": "2.0.0", // La version du serveur
|
|
307
340
|
"port": 3000, // Le port sur lequel lancer le server
|
|
308
341
|
"enableHttps": false, // Activer ou désactiver l'écoute du serveur en HTTPS. Si désactivé, le serveur n'écoutera qu'en HTTP.
|
|
309
342
|
"httpsCertFile": "path/to/https/cert.pem", // Chemin vers le certificat HTTPS
|
|
@@ -322,6 +355,7 @@ Vous pouvez créer un fichier de configuration **local.json** dans le dossier **
|
|
|
322
355
|
"aiUltraModelID": null, // ID du modèle (tel que retourné par l'API à l'URL du dessus) à charger pour l'IA Ultra. Peut rester vide, dans ce cas, le modèle par défaut fourni par l'API sera chargé
|
|
323
356
|
"aiUltraCustomModelURL": null, // Une URL pointant vers un modèle d'IA à charger. Doit être un modèle Tensorflow.js entraîné pour l'IA Ultra. En cas de soucis, l'initialisation du jeu plantera quand des IA Ultra seront dans la partie
|
|
324
357
|
"playerWaitTime": 45000, // Le temps durant lequel attendre la connexion d'autres joueurs à la salle (ms)
|
|
358
|
+
"matchmakingWaitTime": 30000, // Le temps durant lequel attendre avant de recommencer le matchmaking après la fin d'une partie (ms)
|
|
325
359
|
"enableMaxTimeGame": true, // Activer la limite de temps pour chaque partie
|
|
326
360
|
"maxTimeGame": 300000, // La limite de temps pour chaque partie (ms)
|
|
327
361
|
"enableAuthentication": true, // Activer l'authentification lors de la connexion au serveur
|
|
@@ -334,8 +368,12 @@ Vous pouvez créer un fichier de configuration **local.json** dans le dossier **
|
|
|
334
368
|
"recaptchaApiUrl": "https://www.google.com/recaptcha/api/siteverify", // URL de l'API ReCaptcha
|
|
335
369
|
"recaptchaPublicKey": "", // Clé publique ReCaptcha (si non fournie, le ReCaptcha sera désactivé)
|
|
336
370
|
"recaptchaPrivateKey": "", // Clé privée ReCaptcha (si non fournie, le ReCaptcha sera désactivé)
|
|
337
|
-
"authentMaxRequest":
|
|
338
|
-
"authentWindowMs": 900000, //
|
|
371
|
+
"authentMaxRequest": 15, // Nombre maximal de tentatives d'authentification par fenêtre de temps
|
|
372
|
+
"authentWindowMs": 900000, // Durée de la fenêtre de temps (pour l'authentification) en ms (900000 = 15 minutes)
|
|
373
|
+
"adminAuthentMaxRequest": 5, // Nombre maximal de tentatives d'authentification sur l'interface d'administration par fenêtre de temps
|
|
374
|
+
"adminAuthentWindowMs": 900000, // Durée de la fenêtre de temps (pour l'authentification admin) en ms (900000 = 15 minutes)
|
|
375
|
+
"adminActionsMaxRequest": 30, // Nombre maximal d'actions sur l'interface d'administration par fenêtre de temps
|
|
376
|
+
"adminActionsWindowMs": 60000, // Durée de la fenêtre de temps (pour les actions admin) en ms (60000 = 1 minute)
|
|
339
377
|
"ipBan": [], // Une liste d'IPs à bannir
|
|
340
378
|
"usernameBan": [], // Une liste de noms d'utilisateur à bannir
|
|
341
379
|
"contactBan": "", // Une URL de contact à afficher lorsque l'utilisateur est banni
|
|
@@ -355,6 +393,34 @@ Vous pouvez créer un fichier de configuration **local.json** dans le dossier **
|
|
|
355
393
|
|
|
356
394
|
## Journal des changements
|
|
357
395
|
|
|
396
|
+
* Version 2.0.0 (08/05/2026) :
|
|
397
|
+
- ⚠️ Breaking Changes :
|
|
398
|
+
- L'authentification n'est plus compatible avec SnakeIA 3.1.0 et versions inférieures ;
|
|
399
|
+
- Le client doit être mis à jour vers SnakeIA 3.2.0 pour être compatible avec la nouvelle méthode d'authentification ;
|
|
400
|
+
- Une erreur explicite est désormais affichée à l'authentification lorsqu'un client incompatible tente de se connecter ;
|
|
401
|
+
- Mise à jour vers SnakeIA 3.2.0 pour profiter des dernières améliorations ;
|
|
402
|
+
- Le matchmaking se relance automatiquement au bout de 30 secondes après la fin d'une partie (paramétrable via la configuration `matchmakingWaitTime`) ;
|
|
403
|
+
- Correction de bugs et améliorations de sécurité :
|
|
404
|
+
- Correction d'une faille de sécurité dans l'ancien système d'authentification :
|
|
405
|
+
- L'ID du socket était transmis dans l'URL d'authentification ;
|
|
406
|
+
- Un attaquant pouvait alors réutiliser cette URL pour récupérer le token de sa victime ;
|
|
407
|
+
- L'impact restait limité car les comptes sont actuellement éphémères ;
|
|
408
|
+
- Amélioration de la sécurité avec la mise en place d'une protection CSRF sur la plupart des endpoints (auparavant, seuls quelques endpoints étaient couverts) ;
|
|
409
|
+
- Renforcement du système de rate limiting :
|
|
410
|
+
- Règles plus restrictives ;
|
|
411
|
+
- Ajout du rate limiting pour les actions d'administration ;
|
|
412
|
+
- Configuration distincte pour :
|
|
413
|
+
- L'authentification utilisateur ;
|
|
414
|
+
- L'authentification administrateur ;
|
|
415
|
+
- Les actions d'administration ;
|
|
416
|
+
- Correction de bugs avec la récupération du nom d'utilisateur des tokens ;
|
|
417
|
+
- Correction d'un crash du serveur lors de la modification de la configuration via le panneau d'administration/modération si le fichier de configuration était en lecture seule ;
|
|
418
|
+
- Améliorations techniques :
|
|
419
|
+
- Migration vers la librairie Jose pour la gestion des tokens d'authentification ;
|
|
420
|
+
- Améliorations des réponses HTTP (respectent les normes API REST) des différents endpoints ;
|
|
421
|
+
- Refactorisation du code ;
|
|
422
|
+
- Mise à jour des dépendances
|
|
423
|
+
|
|
358
424
|
* Version 2.0.0-beta.1 (07/03/2026) :
|
|
359
425
|
- ⚠️ Breaking Changes :
|
|
360
426
|
- L'authentification n'est plus compatible avec SnakeIA 3.1.0 et versions inférieures
|
package/config/default.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ServerConfig": {
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"port": 3000,
|
|
5
5
|
"enableHttps": false,
|
|
6
6
|
"httpsCertFile": "path/to/https/cert.pem",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"aiUltraModelID": null,
|
|
20
20
|
"aiUltraCustomModelURL": null,
|
|
21
21
|
"playerWaitTime": 60000,
|
|
22
|
+
"matchmakingWaitTime": 30000,
|
|
22
23
|
"enableMaxTimeGame": true,
|
|
23
24
|
"maxTimeGame": 300000,
|
|
24
25
|
"enableAuthentication": true,
|
|
@@ -31,8 +32,12 @@
|
|
|
31
32
|
"recaptchaApiUrl": "https://www.google.com/recaptcha/api/siteverify",
|
|
32
33
|
"recaptchaPublicKey": "",
|
|
33
34
|
"recaptchaPrivateKey": "",
|
|
34
|
-
"authentMaxRequest":
|
|
35
|
+
"authentMaxRequest": 15,
|
|
35
36
|
"authentWindowMs": 900000,
|
|
37
|
+
"adminAuthentMaxRequest": 5,
|
|
38
|
+
"adminAuthentWindowMs": 900000,
|
|
39
|
+
"adminActionsMaxRequest": 30,
|
|
40
|
+
"adminActionsWindowMs": 60000,
|
|
36
41
|
"ipBan": [],
|
|
37
42
|
"usernameBan": [],
|
|
38
43
|
"contactBan": "",
|
package/locales/en.json
CHANGED
|
@@ -61,5 +61,6 @@
|
|
|
61
61
|
"moderator": "Moderator",
|
|
62
62
|
"administrator": "Administrator",
|
|
63
63
|
"role": "Role:",
|
|
64
|
-
"loggedInAs": "Logged in as"
|
|
64
|
+
"loggedInAs": "Logged in as",
|
|
65
|
+
"clientNotCompatible": "Your version of the game is not compatible with this server (server version: %s). Update your game, then try again."
|
|
65
66
|
}
|
package/locales/fr.json
CHANGED
|
@@ -61,5 +61,6 @@
|
|
|
61
61
|
"moderator": "Modérateur",
|
|
62
62
|
"administrator": "Administrateur",
|
|
63
63
|
"role": "Rôle :",
|
|
64
|
-
"loggedInAs": "Connecté en tant que"
|
|
64
|
+
"loggedInAs": "Connecté en tant que",
|
|
65
|
+
"clientNotCompatible": "Votre version du jeu n'est pas compatible avec ce serveur (version serveur : %s). Mettez à jour votre jeu, puis réessayez."
|
|
65
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snakeia-server",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Server for multiplaying in SnakeIA (https://github.com/Eliastik/snakeia)",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,15 +22,16 @@
|
|
|
22
22
|
"config": "^4.4.1",
|
|
23
23
|
"cookie-parser": "^1.4.7",
|
|
24
24
|
"csrf-csrf": "^4.0.3",
|
|
25
|
-
"ejs": "^5.0.
|
|
25
|
+
"ejs": "^5.0.2",
|
|
26
26
|
"express": "^5.2.1",
|
|
27
|
-
"express-rate-limit": "^8.
|
|
27
|
+
"express-rate-limit": "^8.5.1",
|
|
28
28
|
"html-entities": "^2.6.0",
|
|
29
29
|
"i18n": "^0.15.3",
|
|
30
|
-
"jose": "^6.2.
|
|
30
|
+
"jose": "^6.2.3",
|
|
31
31
|
"node-fetch": "^3.3.2",
|
|
32
32
|
"seedrandom": "^3.0.5",
|
|
33
|
-
"
|
|
33
|
+
"semver": "^7.7.4",
|
|
34
|
+
"snakeia": "^3.2.0",
|
|
34
35
|
"socket.io": "^4.8.3",
|
|
35
36
|
"socket.io-cookie-parser": "^1.0.0",
|
|
36
37
|
"winston": "^3.19.0"
|
package/server.js
CHANGED
|
@@ -41,6 +41,7 @@ const { randomUUID,
|
|
|
41
41
|
randomBytes,
|
|
42
42
|
createSecretKey,
|
|
43
43
|
createHash } = require("crypto");
|
|
44
|
+
const semver = require("semver");
|
|
44
45
|
|
|
45
46
|
process.env["ALLOW_CONFIG_MUTATIONS"] = true;
|
|
46
47
|
let config = node_config.get("ServerConfig"); // Server configuration (see default config file config.json)
|
|
@@ -63,8 +64,13 @@ const productionMode = process.env.NODE_ENV === "production";
|
|
|
63
64
|
|
|
64
65
|
// Update config to file
|
|
65
66
|
function updateConfigToFile() {
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// Logging
|
|
@@ -316,6 +322,7 @@ async function createRoom(data, socket) {
|
|
|
316
322
|
started: false,
|
|
317
323
|
alreadyInit: false,
|
|
318
324
|
timeoutPlay: null,
|
|
325
|
+
timeoutMatchmaking: null,
|
|
319
326
|
timeStart: null,
|
|
320
327
|
timeoutMaxTimePlay: null
|
|
321
328
|
};
|
|
@@ -509,7 +516,14 @@ function setupRoom(code) {
|
|
|
509
516
|
if(games[code] != null) {
|
|
510
517
|
games[code].started = false;
|
|
511
518
|
games[code].searchingPlayers = true;
|
|
519
|
+
|
|
512
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);
|
|
513
527
|
}
|
|
514
528
|
});
|
|
515
529
|
|
|
@@ -631,6 +645,11 @@ function cleanRooms() {
|
|
|
631
645
|
}
|
|
632
646
|
|
|
633
647
|
function gameMatchmaking(game, code) {
|
|
648
|
+
if(game.timeoutMatchmaking != null) {
|
|
649
|
+
clearTimeout(game.timeoutMatchmaking);
|
|
650
|
+
game.timeoutMatchmaking = null;
|
|
651
|
+
}
|
|
652
|
+
|
|
634
653
|
if(game != null && games[code] != null && games[code].searchingPlayers) {
|
|
635
654
|
let numberPlayers = game.players.length + games[code].numberAIToAdd;
|
|
636
655
|
|
|
@@ -696,6 +715,11 @@ async function startGame(code) {
|
|
|
696
715
|
game.timeoutPlay = null;
|
|
697
716
|
}
|
|
698
717
|
|
|
718
|
+
if(game.timeoutMatchmaking != null) {
|
|
719
|
+
clearTimeout(game.timeoutMatchmaking);
|
|
720
|
+
game.timeoutMatchmaking = null;
|
|
721
|
+
}
|
|
722
|
+
|
|
699
723
|
game.searchingPlayers = false;
|
|
700
724
|
game.started = true;
|
|
701
725
|
game.gameEngine.snakes = [];
|
|
@@ -941,9 +965,9 @@ const { doubleCsrfProtection: doubleCsrfProtectionUserAuthent, generateCsrfToken
|
|
|
941
965
|
req.query?._csrf
|
|
942
966
|
);
|
|
943
967
|
},
|
|
944
|
-
cookieName:
|
|
968
|
+
cookieName: "snakeia-server.x-csrf-token-user",
|
|
945
969
|
cookieOptions: {
|
|
946
|
-
sameSite:
|
|
970
|
+
sameSite: "lax",
|
|
947
971
|
path: "/authentication",
|
|
948
972
|
secure: productionMode
|
|
949
973
|
}
|
|
@@ -959,6 +983,8 @@ app.use("/authentication", rateLimit({
|
|
|
959
983
|
// IP ban
|
|
960
984
|
app.use(function(req, res, next) {
|
|
961
985
|
if(ipBanned(req.ip)) {
|
|
986
|
+
res.status(403);
|
|
987
|
+
|
|
962
988
|
return res.render(__dirname + "/views/banned.html", {
|
|
963
989
|
contact: config.contactBan,
|
|
964
990
|
theme: req.query.theme
|
|
@@ -978,6 +1004,7 @@ app.get("/", function(req, res) {
|
|
|
978
1004
|
|
|
979
1005
|
app.get("/authentication", async (req, res) => {
|
|
980
1006
|
if(!req.cookies || !config.enableAuthentication) {
|
|
1007
|
+
res.status(400);
|
|
981
1008
|
return res.end();
|
|
982
1009
|
}
|
|
983
1010
|
|
|
@@ -985,6 +1012,12 @@ app.get("/authentication", async (req, res) => {
|
|
|
985
1012
|
|
|
986
1013
|
setSessionCookie(req, res);
|
|
987
1014
|
|
|
1015
|
+
const clientCompatible = isClientCompatible(req.query.version, req.query.id);
|
|
1016
|
+
|
|
1017
|
+
if(!clientCompatible) {
|
|
1018
|
+
res.status(403);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
988
1021
|
res.render(__dirname + "/views/authentication.html", {
|
|
989
1022
|
publicKey: config.recaptchaPublicKey,
|
|
990
1023
|
enableRecaptcha: config.enableRecaptcha,
|
|
@@ -1000,14 +1033,26 @@ app.get("/authentication", async (req, res) => {
|
|
|
1000
1033
|
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1001
1034
|
maxTimeGame: config.maxTimeGame,
|
|
1002
1035
|
theme: req.query.theme,
|
|
1036
|
+
clientCompatible,
|
|
1037
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1003
1038
|
csrfToken: generateCsrfTokenUserAuthent(req, res, { overwrite: true, validateOnReuse: true }),
|
|
1004
1039
|
});
|
|
1005
1040
|
|
|
1006
|
-
if(authenticated) {
|
|
1041
|
+
if(authenticated && clientCompatible) {
|
|
1007
1042
|
sendTokenToSocket(req, getExpressUserToken(req));
|
|
1008
1043
|
}
|
|
1009
1044
|
});
|
|
1010
1045
|
|
|
1046
|
+
function isClientCompatible(version, hasIdQueryParam) {
|
|
1047
|
+
const hasNoVersion = !version || version.trim().length === 0;
|
|
1048
|
+
|
|
1049
|
+
if((hasNoVersion && hasIdQueryParam) || hasNoVersion) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return semver.gte(version, GameConstants.Setting.APP_VERSION);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1011
1056
|
function setSessionCookie(req, res) {
|
|
1012
1057
|
let sessionId = req.cookies.sessionId;
|
|
1013
1058
|
|
|
@@ -1026,6 +1071,7 @@ function setSessionCookie(req, res) {
|
|
|
1026
1071
|
|
|
1027
1072
|
app.post("/authentication", doubleCsrfProtectionUserAuthent, async (req, res) => {
|
|
1028
1073
|
if(!req.cookies || !config.enableAuthentication) {
|
|
1074
|
+
res.status(400);
|
|
1029
1075
|
return res.end();
|
|
1030
1076
|
}
|
|
1031
1077
|
|
|
@@ -1035,11 +1081,17 @@ app.post("/authentication", doubleCsrfProtectionUserAuthent, async (req, res) =>
|
|
|
1035
1081
|
return res.end();
|
|
1036
1082
|
}
|
|
1037
1083
|
|
|
1084
|
+
const clientCompatible = isClientCompatible(req.query.version, req.query.id);
|
|
1085
|
+
|
|
1038
1086
|
let formError = null;
|
|
1039
1087
|
|
|
1040
|
-
|
|
1088
|
+
if(clientCompatible) {
|
|
1089
|
+
await verifyFormAuthentication(req.body).catch(e => formError = e);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if(formError || !clientCompatible) {
|
|
1093
|
+
res.status(403);
|
|
1041
1094
|
|
|
1042
|
-
if(formError) {
|
|
1043
1095
|
return res.render(__dirname + "/views/authentication.html", {
|
|
1044
1096
|
publicKey: config.recaptchaPublicKey,
|
|
1045
1097
|
enableRecaptcha: config.enableRecaptcha,
|
|
@@ -1055,6 +1107,8 @@ app.post("/authentication", doubleCsrfProtectionUserAuthent, async (req, res) =>
|
|
|
1055
1107
|
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1056
1108
|
maxTimeGame: config.maxTimeGame,
|
|
1057
1109
|
theme: req.query.theme,
|
|
1110
|
+
clientCompatible,
|
|
1111
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1058
1112
|
csrfToken: generateCsrfTokenUserAuthent(req, res, { overwrite: true, validateOnReuse: true })
|
|
1059
1113
|
});
|
|
1060
1114
|
}
|
|
@@ -1084,6 +1138,8 @@ app.post("/authentication", doubleCsrfProtectionUserAuthent, async (req, res) =>
|
|
|
1084
1138
|
enableMaxTimeGame: config.enableMaxTimeGame,
|
|
1085
1139
|
maxTimeGame: config.maxTimeGame,
|
|
1086
1140
|
theme: req.query.theme,
|
|
1141
|
+
clientCompatible,
|
|
1142
|
+
serverGameVersion: GameConstants.Setting.APP_VERSION,
|
|
1087
1143
|
csrfToken: null
|
|
1088
1144
|
});
|
|
1089
1145
|
|
|
@@ -1201,13 +1257,21 @@ function manualIPBan(value) {
|
|
|
1201
1257
|
}
|
|
1202
1258
|
|
|
1203
1259
|
function resetLog() {
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
+
}
|
|
1206
1266
|
}
|
|
1207
1267
|
|
|
1208
1268
|
function resetErrorLog() {
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
+
}
|
|
1211
1275
|
}
|
|
1212
1276
|
|
|
1213
1277
|
function updateConfig(value) {
|
|
@@ -1269,8 +1333,21 @@ async function verifyAdminToken(req) {
|
|
|
1269
1333
|
}
|
|
1270
1334
|
}
|
|
1271
1335
|
|
|
1272
|
-
|
|
1336
|
+
const adminAuthentRateLimiter = rateLimit({
|
|
1337
|
+
windowMs: config.adminAuthentWindowMs,
|
|
1338
|
+
max: config.adminAuthentMaxRequest,
|
|
1339
|
+
validate: { trustProxy: false }
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
const adminActionsRateLimiter = rateLimit({
|
|
1343
|
+
windowMs: config.adminActionsWindowMs,
|
|
1344
|
+
max: config.adminActionsMaxRequest,
|
|
1345
|
+
validate: { trustProxy: false }
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
app.get("/admin", adminActionsRateLimiter, doubleCsrfProtectionAdmin, async (req, res) => {
|
|
1273
1349
|
if(!req.cookies) {
|
|
1350
|
+
res.status(400);
|
|
1274
1351
|
return res.end();
|
|
1275
1352
|
}
|
|
1276
1353
|
|
|
@@ -1282,6 +1359,10 @@ app.get("/admin", doubleCsrfProtectionAdmin, async (req, res) => {
|
|
|
1282
1359
|
fs.promises.readFile(config.logFile, "UTF-8").catch(() => ""),
|
|
1283
1360
|
fs.promises.readFile(config.errorLogFile, "UTF-8").catch(() => "")
|
|
1284
1361
|
]);
|
|
1362
|
+
|
|
1363
|
+
if(!authenticated) {
|
|
1364
|
+
res.status(401);
|
|
1365
|
+
}
|
|
1285
1366
|
|
|
1286
1367
|
res.render(__dirname + "/views/admin.html", {
|
|
1287
1368
|
publicKey: config.recaptchaPublicKey,
|
|
@@ -1295,7 +1376,7 @@ app.get("/admin", doubleCsrfProtectionAdmin, async (req, res) => {
|
|
|
1295
1376
|
locale: i18n.getLocale(req),
|
|
1296
1377
|
games: games,
|
|
1297
1378
|
io: io,
|
|
1298
|
-
config: config,
|
|
1379
|
+
config: role === "administrator" ? config : null,
|
|
1299
1380
|
csrfToken: generateCsrfTokenAdmin(req, res, { overwrite: true, validateOnReuse: true }),
|
|
1300
1381
|
serverLog: logFile,
|
|
1301
1382
|
errorLog: errorLogFile,
|
|
@@ -1306,13 +1387,14 @@ app.get("/admin", doubleCsrfProtectionAdmin, async (req, res) => {
|
|
|
1306
1387
|
|
|
1307
1388
|
async function adminAction(req, res, action) {
|
|
1308
1389
|
if(!req.cookies) {
|
|
1390
|
+
res.status(400);
|
|
1309
1391
|
return res.end();
|
|
1310
1392
|
}
|
|
1311
1393
|
|
|
1312
1394
|
const payload = await verifyAdminToken(req);
|
|
1313
1395
|
|
|
1314
1396
|
if(!payload) {
|
|
1315
|
-
return res.redirect("/admin");
|
|
1397
|
+
return res.redirect(303, "/admin");
|
|
1316
1398
|
}
|
|
1317
1399
|
|
|
1318
1400
|
const role = config.adminAccounts[payload.username]["role"] || "moderator";
|
|
@@ -1321,7 +1403,7 @@ async function adminAction(req, res, action) {
|
|
|
1321
1403
|
invalidatedAdminTokens.add(req.cookies.tokenAdmin);
|
|
1322
1404
|
res.clearCookie("tokenAdmin");
|
|
1323
1405
|
|
|
1324
|
-
return res.redirect("/admin");
|
|
1406
|
+
return res.redirect(303, "/admin");
|
|
1325
1407
|
}
|
|
1326
1408
|
|
|
1327
1409
|
const { socket, token, value } = req.body;
|
|
@@ -1359,22 +1441,37 @@ async function adminAction(req, res, action) {
|
|
|
1359
1441
|
unbanIP(value);
|
|
1360
1442
|
break;
|
|
1361
1443
|
case "resetLog":
|
|
1362
|
-
if(role === "administrator")
|
|
1444
|
+
if(role === "administrator") {
|
|
1445
|
+
resetLog();
|
|
1446
|
+
} else {
|
|
1447
|
+
res.status(403);
|
|
1448
|
+
return res.end();
|
|
1449
|
+
}
|
|
1363
1450
|
break;
|
|
1364
1451
|
case "resetErrorLog":
|
|
1365
|
-
if(role === "administrator")
|
|
1452
|
+
if(role === "administrator") {
|
|
1453
|
+
resetErrorLog();
|
|
1454
|
+
} else {
|
|
1455
|
+
res.status(403);
|
|
1456
|
+
return res.end();
|
|
1457
|
+
}
|
|
1366
1458
|
break;
|
|
1367
1459
|
case "updateConfig":
|
|
1368
|
-
if(role === "administrator")
|
|
1460
|
+
if(role === "administrator") {
|
|
1461
|
+
updateConfig(value);
|
|
1462
|
+
} else {
|
|
1463
|
+
res.status(403);
|
|
1464
|
+
return res.end();
|
|
1465
|
+
}
|
|
1369
1466
|
break;
|
|
1370
1467
|
}
|
|
1371
1468
|
|
|
1372
|
-
res.redirect("/admin");
|
|
1469
|
+
res.redirect(303, "/admin");
|
|
1373
1470
|
}
|
|
1374
1471
|
|
|
1375
1472
|
const jsonParser = bodyParser.json();
|
|
1376
1473
|
|
|
1377
|
-
app.post("/admin/:action", jsonParser, doubleCsrfProtectionAdmin, function(req, res) {
|
|
1474
|
+
app.post("/admin/:action", adminActionsRateLimiter, jsonParser, doubleCsrfProtectionAdmin, function(req, res) {
|
|
1378
1475
|
adminAction(req, res, req.params.action);
|
|
1379
1476
|
});
|
|
1380
1477
|
|
|
@@ -1384,26 +1481,23 @@ app.use(function (err, req, res, next) {
|
|
|
1384
1481
|
res.send("Error: invalid CSRF token");
|
|
1385
1482
|
});
|
|
1386
1483
|
|
|
1387
|
-
|
|
1388
|
-
windowMs: config.authentWindowMs,
|
|
1389
|
-
max: config.authentMaxRequest,
|
|
1390
|
-
validate: { trustProxy: false }
|
|
1391
|
-
});
|
|
1392
|
-
|
|
1393
|
-
app.post("/admin", adminRateLimiter, async (req, res) => {
|
|
1484
|
+
app.post("/admin", adminAuthentRateLimiter, async (req, res) => {
|
|
1394
1485
|
if(!req.cookies) {
|
|
1486
|
+
res.status(400);
|
|
1395
1487
|
return res.end();
|
|
1396
1488
|
}
|
|
1397
1489
|
|
|
1398
1490
|
const payload = await verifyAdminToken(req);
|
|
1399
1491
|
|
|
1400
1492
|
if(payload) {
|
|
1401
|
-
return res.redirect("/admin");
|
|
1493
|
+
return res.redirect(303, "/admin");
|
|
1402
1494
|
}
|
|
1403
1495
|
|
|
1404
1496
|
try {
|
|
1405
1497
|
await verifyFormAuthenticationAdmin(req.body);
|
|
1406
1498
|
} catch(err) {
|
|
1499
|
+
res.status(403);
|
|
1500
|
+
|
|
1407
1501
|
return res.render(__dirname + "/views/admin.html", {
|
|
1408
1502
|
publicKey: config.recaptchaPublicKey,
|
|
1409
1503
|
enableRecaptcha: config.enableRecaptcha,
|
|
@@ -1437,7 +1531,7 @@ app.post("/admin", adminRateLimiter, async (req, res) => {
|
|
|
1437
1531
|
secure: req.protocol === "https"
|
|
1438
1532
|
});
|
|
1439
1533
|
|
|
1440
|
-
res.redirect("/admin");
|
|
1534
|
+
res.redirect(303, "/admin");
|
|
1441
1535
|
|
|
1442
1536
|
logger.info("admin authent - username: " + username + " - ip: " + req.ip);
|
|
1443
1537
|
});
|
|
@@ -1646,7 +1740,6 @@ io.on("connection", async (socket) => {
|
|
|
1646
1740
|
});
|
|
1647
1741
|
|
|
1648
1742
|
logger.info("join room (code: " + code + ") - username: " + (await Player.getUsernameSocket(socket, jsonWebTokenSecretKey)) + " - ip: " + getIPSocketIO(socket.handshake) + " - socket: " + socket.id);
|
|
1649
|
-
|
|
1650
1743
|
} else {
|
|
1651
1744
|
if(games[code] == null) {
|
|
1652
1745
|
socket.emit("join-room", { success: false, errorCode: GameConstants.Error.ROOM_NOT_FOUND });
|
package/views/admin.html
CHANGED
|
@@ -271,8 +271,18 @@
|
|
|
271
271
|
body: JSON.stringify(data),
|
|
272
272
|
redirect: "follow"
|
|
273
273
|
}).then((response) => {
|
|
274
|
-
|
|
274
|
+
const responseOK = response.ok || response.status === 401;
|
|
275
|
+
const redirected = response.redirected;
|
|
276
|
+
|
|
277
|
+
if(responseOK && redirected) {
|
|
275
278
|
location.href = response.url;
|
|
279
|
+
} else if(!responseOK) {
|
|
280
|
+
element.classList.remove("disabled");
|
|
281
|
+
element.disabled = false;
|
|
282
|
+
|
|
283
|
+
if(response.status === 403) {
|
|
284
|
+
location.href = "/admin";
|
|
285
|
+
}
|
|
276
286
|
}
|
|
277
287
|
}).catch(() => {
|
|
278
288
|
element.classList.remove("disabled");
|
|
@@ -302,7 +312,7 @@
|
|
|
302
312
|
if((confirmAction && confirm("<%= __("actionConfirmAdmin") %>")) || !confirmAction) {
|
|
303
313
|
element.classList.add("disabled");
|
|
304
314
|
element.disabled = true;
|
|
305
|
-
requestAction(action, data);
|
|
315
|
+
requestAction(action, data, element);
|
|
306
316
|
}
|
|
307
317
|
});
|
|
308
318
|
});
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
<body class="text-center remove-padding">
|
|
37
37
|
<% } %>
|
|
38
38
|
<div class="container">
|
|
39
|
-
<% if(
|
|
39
|
+
<% if(!clientCompatible) { %>
|
|
40
|
+
<h3><%= __("clientNotCompatible", serverGameVersion) %></h3>
|
|
41
|
+
<% } else if(authent) { %>
|
|
40
42
|
<h3><%= __("alreadyAuthentified") %></h3>
|
|
41
43
|
<% } else if(!success) { %>
|
|
42
44
|
<form action="" method="post">
|