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 CHANGED
@@ -15,5 +15,5 @@ steps:
15
15
  password:
16
16
  from_secret: REGISTRY_PASSWORD
17
17
  tags:
18
- - 2.0.0-beta.1
18
+ - 2.0.0
19
19
  - latest
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-beta.1 (3/7/2026)
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-beta.1", // The server version
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": 50, // Maximum request for authentication
109
- "authentWindowMs": 900000, // Time when the authentication requests are saved (ms)
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-beta.1 (07/03/2026)
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-beta.1", // La version du serveur
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": 50, // Nombre maximal de requêtes lors de l'authentification
338
- "authentWindowMs": 900000, // Temps durant lequel les tentatives d'authentification seront enregistrées (ms)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "ServerConfig": {
3
- "version": "2.0.0-beta.1",
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": 50,
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-beta.1",
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.1",
25
+ "ejs": "^5.0.2",
26
26
  "express": "^5.2.1",
27
- "express-rate-limit": "^8.3.0",
27
+ "express-rate-limit": "^8.5.1",
28
28
  "html-entities": "^2.6.0",
29
29
  "i18n": "^0.15.3",
30
- "jose": "^6.2.0",
30
+ "jose": "^6.2.3",
31
31
  "node-fetch": "^3.3.2",
32
32
  "seedrandom": "^3.0.5",
33
- "snakeia": "^3.1.0",
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
- fs.writeFileSync(configFile, JSON.stringify({ "ServerConfig": config }, null, 4), "UTF-8");
67
- config = node_config.get("ServerConfig");
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: productionMode ? "__Host-snakeia-server.x-csrf-token-user" : "snakeia-server.x-csrf-token-user",
968
+ cookieName: "snakeia-server.x-csrf-token-user",
945
969
  cookieOptions: {
946
- sameSite: productionMode ? "strict" : "lax",
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
- await verifyFormAuthentication(req.body).catch(e => formError = e);
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
- fs.writeFileSync(config.logFile, "", "UTF-8");
1205
- logger.info("log file reseted");
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
- fs.writeFileSync(config.errorLogFile, "", "UTF-8");
1210
- logger.info("error log file reseted");
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
- app.get("/admin", doubleCsrfProtectionAdmin, async (req, res) => {
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") resetLog();
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") resetErrorLog();
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") updateConfig(value);
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
- const adminRateLimiter = rateLimit({
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
- if(response.ok && response.redirected) {
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(authent) { %>
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">