@frangoteam/fuxa-min 1.2.10 → 1.3.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.
Files changed (60) hide show
  1. package/api/auth/index.js +141 -3
  2. package/api/command/index.js +10 -4
  3. package/api/diagnose/index.js +12 -4
  4. package/api/index.js +41 -8
  5. package/api/jwt-helper.js +15 -2
  6. package/api/path-helper.js +41 -0
  7. package/api/projects/index.js +27 -14
  8. package/api/reports/reports.service.ts +12 -2
  9. package/api/resources/index.js +30 -9
  10. package/api/scheduler/index.js +21 -1
  11. package/dist/3rdpartylicenses.txt +139 -7
  12. package/dist/assets/i18n/de.json +10 -0
  13. package/dist/assets/i18n/en.json +17 -3
  14. package/dist/assets/i18n/es.json +12 -0
  15. package/dist/assets/i18n/fr.json +10 -0
  16. package/dist/assets/i18n/ja.json +15 -6
  17. package/dist/assets/i18n/ko.json +12 -0
  18. package/dist/assets/i18n/pt.json +9 -2
  19. package/dist/assets/i18n/ru.json +11 -0
  20. package/dist/assets/i18n/sv.json +10 -1
  21. package/dist/assets/i18n/tr.json +8 -1
  22. package/dist/assets/i18n/ua.json +9 -2
  23. package/dist/assets/i18n/zh-cn.json +10 -0
  24. package/dist/assets/i18n/zh-tw.json +11 -1
  25. package/dist/index.html +2 -2
  26. package/dist/main.bafae830903d548e.js +329 -0
  27. package/dist/polyfills.d7de05f9af2fb559.js +1 -0
  28. package/dist/reports.service.js +11 -1
  29. package/dist/{runtime.8ef63094e52a66ba.js → runtime.9136a61a9b98f987.js} +1 -1
  30. package/dist/{scripts.40b60f02658462e4.js → scripts.d9e6ee984bf6f3b7.js} +1 -1
  31. package/dist/styles.545e37beb3e671ba.css +1 -0
  32. package/integrations/node-red/index.js +91 -24
  33. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +56 -5
  34. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +8 -2
  35. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +56 -5
  36. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +12 -12
  37. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +56 -5
  38. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +14 -10
  39. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +56 -5
  40. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +8 -2
  41. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +56 -5
  42. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +24 -20
  43. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +56 -5
  44. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +8 -2
  45. package/main.js +41 -17
  46. package/package.json +10 -5
  47. package/runtime/devices/adsclient/index.js +1 -1
  48. package/runtime/devices/bacnet/index.js +66 -32
  49. package/runtime/devices/ethernetip/index.js +1 -1
  50. package/runtime/devices/gpio/index.js +1 -1
  51. package/runtime/devices/odbc/index.js +5 -5
  52. package/runtime/devices/template/index.js +14 -14
  53. package/runtime/storage/daqstorage.js +28 -2
  54. package/runtime/storage/influxdb/index.js +1 -1
  55. package/runtime/storage/questdb/index.js +224 -0
  56. package/runtime/utils.js +5 -0
  57. package/settings.default.js +13 -3
  58. package/dist/main.020ca34630a3363a.js +0 -329
  59. package/dist/polyfills.c8e7db9850a3ad8b.js +0 -1
  60. package/dist/styles.03cc550382689976.css +0 -1
package/api/auth/index.js CHANGED
@@ -10,12 +10,78 @@ const authJwt = require('../jwt-helper');
10
10
  var runtime;
11
11
  var secretCode;
12
12
  var tokenExpiresIn;
13
+ var enableRefreshCookieAuth = false;
14
+ var refreshTokenExpiresIn = '7d';
15
+ const refreshCookieName = 'fuxa_refresh';
16
+
17
+ function parseExpiresToMs(expiresIn) {
18
+ if (expiresIn === undefined || expiresIn === null) {
19
+ return null;
20
+ }
21
+ if (typeof expiresIn === 'number') {
22
+ return expiresIn * 1000;
23
+ }
24
+ if (typeof expiresIn !== 'string') {
25
+ return null;
26
+ }
27
+ const match = expiresIn.trim().match(/^(\d+)\s*([smhd])?$/i);
28
+ if (!match) {
29
+ return null;
30
+ }
31
+ const value = Number(match[1]);
32
+ const unit = (match[2] || 's').toLowerCase();
33
+ const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
34
+ return value * (multipliers[unit] || 1000);
35
+ }
36
+
37
+ function getCookieValue(req, name) {
38
+ const cookieHeader = req.headers.cookie;
39
+ if (!cookieHeader) return null;
40
+ const cookies = cookieHeader.split(';');
41
+ for (const cookie of cookies) {
42
+ const [key, ...rest] = cookie.trim().split('=');
43
+ if (key === name) {
44
+ return decodeURIComponent(rest.join('='));
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function buildAccessToken(user) {
51
+ return jwt.sign({ id: user.username, groups: user.groups }, secretCode, { expiresIn: tokenExpiresIn });
52
+ }
53
+
54
+ function buildRefreshToken(user) {
55
+ return jwt.sign({ id: user.username, groups: user.groups, type: 'refresh' }, secretCode, { expiresIn: refreshTokenExpiresIn });
56
+ }
57
+
58
+ function setRefreshCookie(res, token) {
59
+ const maxAge = parseExpiresToMs(refreshTokenExpiresIn);
60
+ const options = {
61
+ httpOnly: true,
62
+ sameSite: 'lax',
63
+ secure: !!runtime?.settings?.https,
64
+ path: '/api/refresh'
65
+ };
66
+ if (maxAge) {
67
+ options.maxAge = maxAge;
68
+ }
69
+ res.cookie(refreshCookieName, token, options);
70
+ }
71
+
72
+ function clearRefreshCookie(res) {
73
+ res.clearCookie(refreshCookieName, { path: '/api/refresh' });
74
+ }
13
75
 
14
76
  module.exports = {
15
- init: function (_runtime, _secretCode, _tokenExpires) {
77
+ init: function (_runtime, _secretCode, _tokenExpires, _enableRefreshCookieAuth, _refreshTokenExpires) {
16
78
  runtime = _runtime;
17
79
  secretCode = _secretCode;
18
80
  tokenExpiresIn = _tokenExpires;
81
+ enableRefreshCookieAuth = !!_enableRefreshCookieAuth;
82
+ if (_refreshTokenExpires) {
83
+ refreshTokenExpiresIn = _refreshTokenExpires;
84
+ }
19
85
  },
20
86
  app: function () {
21
87
  var authApp = express();
@@ -35,7 +101,11 @@ module.exports = {
35
101
  runtime.users.findOne(req.body).then(function (userInfo) {
36
102
  if (userInfo && userInfo.length && userInfo[0].password) {
37
103
  if (bcrypt.compareSync(req.body.password, userInfo[0].password)) {
38
- const token = jwt.sign({ id: userInfo[0].username, groups: userInfo[0].groups }, secretCode, { expiresIn: tokenExpiresIn });//'1h' });
104
+ const token = buildAccessToken(userInfo[0]);
105
+ if (enableRefreshCookieAuth) {
106
+ const refreshToken = buildRefreshToken(userInfo[0]);
107
+ setRefreshCookie(res, refreshToken);
108
+ }
39
109
  res.json({
40
110
  status: 'success',
41
111
  message: 'user found!!!',
@@ -66,6 +136,74 @@ module.exports = {
66
136
  });
67
137
  });
68
138
 
139
+ /**
140
+ * POST Refresh
141
+ * Refresh access token using HttpOnly refresh cookie
142
+ */
143
+ authApp.post('/api/refresh', async function (req, res, next) {
144
+ if (!runtime?.settings?.secureEnabled || !enableRefreshCookieAuth) {
145
+ return res.status(204).end();
146
+ }
147
+ const refreshToken = getCookieValue(req, refreshCookieName);
148
+ if (!refreshToken) {
149
+ return res.status(401).json({ status: 'error', message: 'Refresh token missing' });
150
+ }
151
+ try {
152
+ const decoded = jwt.verify(refreshToken, secretCode);
153
+ if (decoded?.type !== 'refresh') {
154
+ clearRefreshCookie(res);
155
+ return res.status(401).json({ status: 'error', message: 'Invalid refresh token' });
156
+ }
157
+
158
+ let userData = null;
159
+ try {
160
+ const users = await runtime.users.getUsers({ username: decoded.id });
161
+ if (users && users.length) {
162
+ userData = users[0];
163
+ }
164
+ } catch (err) {
165
+ runtime.logger.error(`api refresh: user lookup failed ${err}`);
166
+ }
167
+
168
+ const user = {
169
+ username: decoded.id,
170
+ fullname: userData?.fullname,
171
+ groups: userData?.groups || decoded.groups,
172
+ info: userData?.info
173
+ };
174
+
175
+ const newAccessToken = buildAccessToken(user);
176
+ const newRefreshToken = buildRefreshToken(user);
177
+ setRefreshCookie(res, newRefreshToken);
178
+
179
+ res.json({
180
+ status: 'success',
181
+ message: 'token refreshed',
182
+ data: {
183
+ username: user.username,
184
+ fullname: user.fullname,
185
+ groups: user.groups,
186
+ info: user.info,
187
+ token: newAccessToken
188
+ }
189
+ });
190
+ } catch (err) {
191
+ clearRefreshCookie(res);
192
+ res.status(401).json({ status: 'error', message: 'Invalid refresh token' });
193
+ }
194
+ });
195
+
196
+ /**
197
+ * POST SignOut
198
+ * Clear refresh cookie
199
+ */
200
+ authApp.post('/api/signout', function (req, res, next) {
201
+ if (enableRefreshCookieAuth) {
202
+ clearRefreshCookie(res);
203
+ }
204
+ res.status(204).end();
205
+ });
206
+
69
207
  return authApp;
70
208
  }
71
- }
209
+ }
@@ -6,6 +6,7 @@ var express = require("express");
6
6
  const authJwt = require('../jwt-helper');
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const { resolveWithin } = require('../path-helper');
9
10
  var runtime;
10
11
  var secureFnc;
11
12
  var checkGroupsFnc;
@@ -39,11 +40,16 @@ module.exports = {
39
40
  } else {
40
41
  if (req.query.cmd === CommanTypeEnum.reportDownload) {
41
42
  try {
42
- const fileName = req.query.name.replace(new RegExp('../', 'g'), '');
43
- var reportPath = path.join(runtime.settings.reportsDir, fileName);
44
- if (!fs.existsSync(reportPath)) {
45
- reportPath = path.join(process.cwd(), runtime.settings.reportsDir, fileName);
43
+ var reportBase = runtime.settings.reportsDir;
44
+ if (!fs.existsSync(reportBase)) {
45
+ reportBase = path.join(process.cwd(), runtime.settings.reportsDir);
46
46
  }
47
+ const resolvedReport = resolveWithin(reportBase, req.query.name);
48
+ if (!resolvedReport) {
49
+ res.status(400).json({ error: "invalid_path", message: "Invalid report file." });
50
+ return;
51
+ }
52
+ const reportPath = resolvedReport.resolvedTarget;
47
53
  if (fs.existsSync(reportPath)) {
48
54
  res.sendFile(reportPath, (err) => {
49
55
  if (err) {
@@ -6,6 +6,7 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  var express = require("express");
8
8
  const authJwt = require('../jwt-helper');
9
+ const { normalizeRelativePath, resolveWithin } = require('../path-helper');
9
10
  var runtime;
10
11
  var secureFnc;
11
12
  var checkGroupsFnc;
@@ -67,14 +68,21 @@ module.exports = {
67
68
  runtime.logger.error("api get logs: Unauthorized!");
68
69
  } else {
69
70
  try {
70
- const fileName = req.query.file.replace(new RegExp('../', 'g'), '');
71
- var logFileName = fileName || 'fuxa.log';
71
+ const requestedName = req.query.file ? normalizeRelativePath(req.query.file) : 'fuxa.log';
72
+ if (!requestedName) {
73
+ res.status(400).json({ error: "invalid_path", message: "Invalid log file." });
74
+ return;
75
+ }
72
76
  var logPath = runtime.logger.logDir();
73
77
  if (!fs.existsSync(logPath)) {
74
78
  logPath = path.join(process.cwd(), runtime.logger.logDir());
75
79
  }
76
- var logFiles = fs.readdirSync(logPath);
77
- let logFile = path.join(logPath, logFileName);
80
+ const resolvedLog = resolveWithin(logPath, requestedName);
81
+ if (!resolvedLog) {
82
+ res.status(400).json({ error: "invalid_path", message: "Invalid log file." });
83
+ return;
84
+ }
85
+ let logFile = resolvedLog.resolvedTarget;
78
86
  res.header('Content-Type', 'text/plain; charset=utf-8');
79
87
  res.download(logFile, (err) => {
80
88
  if (err) {
package/api/index.js CHANGED
@@ -39,8 +39,11 @@ function init(_server, _runtime) {
39
39
  return new Promise(function (resolve, reject) {
40
40
  if (runtime.settings.disableServer !== false) {
41
41
  apiApp = express();
42
- apiApp.use(morgan(['combined', 'common', 'dev', 'short', 'tiny'].
43
- includes(runtime.settings.logApiLevel) ? runtime.settings.logApiLevel : 'combined'));
42
+
43
+ if (runtime.settings.logApiLevel !== 'none') {
44
+ apiApp.use(morgan(['combined', 'common', 'dev', 'short', 'tiny'].
45
+ includes(runtime.settings.logApiLevel) ? runtime.settings.logApiLevel : 'combined'));
46
+ }
44
47
 
45
48
  var maxApiRequestSize = runtime.settings.apiMaxLength || '100mb';
46
49
  apiApp.use(bodyParser.json({limit:maxApiRequestSize}));
@@ -53,7 +56,7 @@ function init(_server, _runtime) {
53
56
  apiApp.use(usersApi.app());
54
57
  alarmsApi.init(runtime, authMiddleware, verifyGroups);
55
58
  apiApp.use(alarmsApi.app());
56
- authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn);
59
+ authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn, runtime.settings.enableRefreshCookieAuth, runtime.settings.refreshTokenExpiresIn);
57
60
  apiApp.use(authApi.app());
58
61
  pluginsApi.init(runtime, authMiddleware, verifyGroups);
59
62
  apiApp.use(pluginsApi.app());
@@ -76,7 +79,9 @@ function init(_server, _runtime) {
76
79
 
77
80
  const limiter = rateLimit({
78
81
  windowMs: 5 * 60 * 1000, // 5 minutes
79
- max: 100 // limit each IP to 100 requests per windowMs
82
+ max: 100, // limit each IP to 100 requests per windowMs
83
+ // Keep lightweight health/version checks unthrottled
84
+ skip: (req) => req.path === '/api/version'
80
85
  });
81
86
 
82
87
  // apply to all requests
@@ -141,18 +146,31 @@ function init(_server, _runtime) {
141
146
  if (!req.body.secretCode && runtime.settings.secretCode) {
142
147
  req.body.secretCode = runtime.settings.secretCode;
143
148
  }
149
+ if (req.body.secureEnabled && !req.body.secretCode) {
150
+ req.body.secretCode = utils.generateSecretCode();
151
+ runtime.logger.warn('Generated random JWT secret because secureEnabled=true and no secretCode was provided.');
152
+ }
144
153
  const prevAuth = {
145
154
  secureEnabled: runtime.settings.secureEnabled,
146
155
  tokenExpiresIn: runtime.settings.tokenExpiresIn,
156
+ enableRefreshCookieAuth: runtime.settings.enableRefreshCookieAuth,
157
+ refreshTokenExpiresIn: runtime.settings.refreshTokenExpiresIn,
147
158
  secretCode: runtime.settings.secretCode
148
159
  };
160
+ if (req.body.nodeRedEnabled === true &&
161
+ utils.isNullOrUndefined(req.body.nodeRedAuthMode) &&
162
+ runtime.settings.nodeRedEnabled === false) {
163
+ req.body.nodeRedAuthMode = 'secure';
164
+ }
149
165
  fs.writeFileSync(runtime.settings.userSettingsFile, JSON.stringify(req.body, null, 4));
150
166
  mergeUserSettings(req.body);
151
167
  if (prevAuth.secureEnabled !== runtime.settings.secureEnabled ||
152
168
  prevAuth.tokenExpiresIn !== runtime.settings.tokenExpiresIn ||
169
+ prevAuth.enableRefreshCookieAuth !== runtime.settings.enableRefreshCookieAuth ||
170
+ prevAuth.refreshTokenExpiresIn !== runtime.settings.refreshTokenExpiresIn ||
153
171
  prevAuth.secretCode !== runtime.settings.secretCode) {
154
172
  authJwt.init(runtime.settings.secureEnabled, runtime.settings.secretCode, runtime.settings.tokenExpiresIn);
155
- authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn);
173
+ authApi.init(runtime, authJwt.secretCode, authJwt.tokenExpiresIn, runtime.settings.enableRefreshCookieAuth, runtime.settings.refreshTokenExpiresIn);
156
174
  }
157
175
  runtime.restart(true).then(function(result) {
158
176
  res.end();
@@ -168,11 +186,11 @@ function init(_server, _runtime) {
168
186
  * GET Heartbeat to check token
169
187
  */
170
188
  apiApp.post('/api/heartbeat', authMiddleware, function (req, res) {
189
+
171
190
  if (!runtime.settings.secureEnabled) {
172
- res.end();
173
- } else if (res.statusCode === 403) {
174
- runtime.logger.error("api post heartbeat: Tocken Expired");
191
+ return res.end();
175
192
  }
193
+
176
194
  if (req.body.params) {
177
195
 
178
196
  if (!req.isAuthenticated) {
@@ -196,6 +214,7 @@ function init(_server, _runtime) {
196
214
  token: authJwt.getGuestToken()
197
215
  });
198
216
  }
217
+
199
218
  return res.end();
200
219
  });
201
220
 
@@ -215,12 +234,26 @@ function mergeUserSettings(settings) {
215
234
  runtime.settings.logFull = settings.logFull;
216
235
  runtime.settings.userRole = settings.userRole;
217
236
  runtime.settings.nodeRedEnabled = settings.nodeRedEnabled;
237
+ if (!utils.isNullOrUndefined(settings.nodeRedAuthMode)) {
238
+ runtime.settings.nodeRedAuthMode = settings.nodeRedAuthMode;
239
+ }
240
+ if (!utils.isNullOrUndefined(settings.enableRefreshCookieAuth)) {
241
+ runtime.settings.enableRefreshCookieAuth = settings.enableRefreshCookieAuth;
242
+ }
243
+ if (!utils.isNullOrUndefined(settings.refreshTokenExpiresIn)) {
244
+ runtime.settings.refreshTokenExpiresIn = settings.refreshTokenExpiresIn;
245
+ }
246
+ if (!utils.isNullOrUndefined(settings.nodeRedUnsafeModules)) {
247
+ runtime.settings.nodeRedUnsafeModules = settings.nodeRedUnsafeModules;
248
+ }
218
249
  runtime.settings.swaggerEnabled = settings.swaggerEnabled;
219
250
  if (settings.secretCode) {
220
251
  runtime.settings.secretCode = settings.secretCode;
221
252
  }
222
253
  if (settings.secureEnabled) {
223
254
  runtime.settings.tokenExpiresIn = settings.tokenExpiresIn;
255
+ runtime.settings.enableRefreshCookieAuth = settings.enableRefreshCookieAuth;
256
+ runtime.settings.refreshTokenExpiresIn = settings.refreshTokenExpiresIn;
224
257
  }
225
258
  if (settings.smtp) {
226
259
  runtime.settings.smtp = settings.smtp;
package/api/jwt-helper.js CHANGED
@@ -1,9 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const jwt = require('jsonwebtoken');
4
+ const utils = require('../runtime/utils');
4
5
 
5
6
  var secureEnabled = false;
6
- var secretCode = 'frangoteam751';
7
+ // Runtime fallback secret used only when no persistent secret is configured.
8
+ var secretCode = utils.generateSecretCode();
7
9
  var tokenExpiresIn = 60 * 60; // 60 minutes
8
10
  const adminGroups = [-1, 255];
9
11
 
@@ -132,6 +134,16 @@ function getGuestToken() {
132
134
  return token;
133
135
  }
134
136
 
137
+ function isGuestUser(userId, userGroups) {
138
+ if (userId === 'guest') {
139
+ return true;
140
+ }
141
+ if (Array.isArray(userGroups) && userGroups.includes('guest')) {
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+
135
147
  function haveAdminPermission(permission) {
136
148
  if (permission === null || permission === undefined) {
137
149
  return false;
@@ -156,5 +168,6 @@ module.exports = {
156
168
  get secretCode() { return secretCode },
157
169
  get tokenExpiresIn() { return tokenExpiresIn },
158
170
  haveAdminPermission: haveAdminPermission,
159
- adminGroups: adminGroups
171
+ adminGroups: adminGroups,
172
+ isGuestUser: isGuestUser
160
173
  };
@@ -0,0 +1,41 @@
1
+ const path = require('path');
2
+
3
+ function normalizeRelativePath(input) {
4
+ if (typeof input !== 'string' || !input) {
5
+ return null;
6
+ }
7
+ if (input.indexOf('\0') !== -1) {
8
+ return null;
9
+ }
10
+ if (path.isAbsolute(input)) {
11
+ return null;
12
+ }
13
+ const normalized = path.normalize(input).replace(/^[\\/]+/, '');
14
+ if (!normalized || normalized === '.' || normalized === path.sep) {
15
+ return null;
16
+ }
17
+ const parts = normalized.split(path.sep);
18
+ if (parts.includes('..')) {
19
+ return null;
20
+ }
21
+ return normalized;
22
+ }
23
+
24
+ function resolveWithin(baseDir, targetPath) {
25
+ const normalized = normalizeRelativePath(targetPath);
26
+ if (!normalized) {
27
+ return null;
28
+ }
29
+ const resolvedBase = path.resolve(baseDir);
30
+ const resolvedTarget = path.resolve(resolvedBase, normalized);
31
+ const relative = path.relative(resolvedBase, resolvedTarget);
32
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
33
+ return null;
34
+ }
35
+ return { resolvedTarget, normalized };
36
+ }
37
+
38
+ module.exports = {
39
+ normalizeRelativePath,
40
+ resolveWithin
41
+ };
@@ -7,6 +7,7 @@ const authJwt = require('../jwt-helper');
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
+ const { normalizeRelativePath, resolveWithin } = require('../path-helper');
10
11
 
11
12
  var runtime;
12
13
  var secureFnc;
@@ -207,41 +208,53 @@ module.exports = {
207
208
  let encoding = {};
208
209
  // let basedata = file.data.replace(/^data:.*,/, '');
209
210
  // let basedata = file.data.replace(/^data:image\/png;base64,/, "");
210
- let fileName = file.name.replace(new RegExp('../', 'g'), '');
211
- let fullPath = file.fullPath || file.name;
212
- fullPath = fullPath.replace(/(\.\.[/\\])/g, '');
213
- fullPath = path.normalize(fullPath).replace(/^(\.\.[/\\])+/, '');
211
+ const rawFileName = typeof file.name === 'string' ? file.name : '';
212
+ const safeFileName = normalizeRelativePath(rawFileName);
213
+ const safeFullPath = normalizeRelativePath(file.fullPath || rawFileName);
214
+ const relativePath = safeFullPath || safeFileName;
215
+ if (!relativePath) {
216
+ res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
217
+ return;
218
+ }
214
219
 
215
220
  if (file.type !== 'svg') {
216
221
  basedata = file.data.replace(/^data:.*,/, '');
217
222
  encoding = {encoding: 'base64'};
218
223
  }
219
- let filePath = path.join(runtime.settings.uploadFileDir, fullPath || fileName);
224
+ const resolvedUpload = resolveWithin(runtime.settings.uploadFileDir, relativePath);
225
+ if (!resolvedUpload) {
226
+ res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
227
+ return;
228
+ }
229
+ let filePath = resolvedUpload.resolvedTarget;
220
230
  if (destination) {
221
231
  const baseDir = process.versions.electron
222
232
  ? (process.env.userDir || path.join(os.homedir(), '.fuxa'))
223
233
  : runtime.settings.appDir;
224
- const normalizedDestination = path.normalize(destination).replace(/^([/\\])+/, '');
225
- const destinationParts = normalizedDestination.split(path.sep);
226
- const hasTraversal = destinationParts.includes('..');
227
- if (!normalizedDestination || hasTraversal || path.isAbsolute(destination)) {
234
+ const normalizedDestination = normalizeRelativePath(destination);
235
+ if (!normalizedDestination) {
228
236
  res.status(400).json({error:"invalid_destination", message: "Invalid destination path."});
229
237
  return;
230
238
  }
231
- const destinationDir = path.resolve(baseDir, `_${normalizedDestination}`);
232
- const resolvedBase = path.resolve(baseDir);
233
- if (destinationDir !== resolvedBase && !destinationDir.startsWith(resolvedBase + path.sep)) {
239
+ const resolvedDestination = resolveWithin(baseDir, `_${normalizedDestination}`);
240
+ if (!resolvedDestination) {
234
241
  res.status(400).json({error:"invalid_destination", message: "Invalid destination path."});
235
242
  return;
236
243
  }
237
- filePath = path.join(destinationDir, fullPath || fileName);
244
+ const destinationDir = resolvedDestination.resolvedTarget;
245
+ const resolvedFile = resolveWithin(destinationDir, relativePath);
246
+ if (!resolvedFile) {
247
+ res.status(400).json({error:"invalid_path", message: "Invalid upload path."});
248
+ return;
249
+ }
250
+ filePath = resolvedFile.resolvedTarget;
238
251
  const dir = path.dirname(filePath);
239
252
  if (!fs.existsSync(dir)) {
240
253
  fs.mkdirSync(dir, { recursive: true });
241
254
  }
242
255
  }
243
256
  fs.writeFileSync(filePath, basedata, encoding);
244
- let result = {'location': '/' + runtime.settings.httpUploadFileStatic + '/' + fullPath || fileName };
257
+ let result = {'location': '/' + runtime.settings.httpUploadFileStatic + '/' + relativePath };
245
258
  res.json(result);
246
259
  } catch (err) {
247
260
  if (err && err.code) {
@@ -2,6 +2,7 @@
2
2
  const authJwt = require('../api/jwt-helper');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { resolveWithin } = require('../api/path-helper');
5
6
  import express, { Request, Response } from 'express';
6
7
 
7
8
  export class ReportsApiService {
@@ -121,7 +122,16 @@ export class ReportsApiService {
121
122
  if (!fs.existsSync(reportPath)) {
122
123
  reportPath = path.join(process.cwd(), this.runtime.settings.reportsDir, req.params);
123
124
  }
124
- const filePath = path.join(reportPath, req.body.params?.fileName);
125
+ const resolvedFile = resolveWithin(reportPath, req.body.params?.fileName);
126
+ if (!resolvedFile) {
127
+ res.status(400).json({ error: "invalid_path", message: "Invalid report file." });
128
+ return;
129
+ }
130
+ const filePath = resolvedFile.resolvedTarget;
131
+ if (!fs.existsSync(filePath)) {
132
+ res.status(404).json({ error: "not_found", message: "Report file not found!" });
133
+ return;
134
+ }
125
135
  fs.unlinkSync(filePath);
126
136
  this.runtime.logger.info(`Report file '${filePath}' deleted!`, true);
127
137
  res.end();
@@ -170,4 +180,4 @@ export interface ReportFile {
170
180
  fileName: string;
171
181
  reportName: string;
172
182
  created: Date;
173
- }
183
+ }
@@ -9,6 +9,7 @@ const authJwt = require('../jwt-helper');
9
9
  const Report = require('../../runtime/jobs/report');
10
10
  const fontkit = require('fontkit');
11
11
  const os = require('os');
12
+ const { resolveWithin } = require('../path-helper');
12
13
 
13
14
  var runtime;
14
15
  var secureFnc;
@@ -116,8 +117,12 @@ module.exports = {
116
117
  runtime.logger.error("api post remove resource: Unauthorized");
117
118
  } else {
118
119
  try {
119
- let fileName = req.body.file.replace(new RegExp('../', 'g'), '');
120
- const filePath = path.join(runtime.settings.resourcesFileDir, fileName);
120
+ const resolvedFile = resolveWithin(runtime.settings.resourcesFileDir, req.body.file);
121
+ if (!resolvedFile) {
122
+ res.status(400).json({ error: "invalid_path", message: "Invalid resource path." });
123
+ return;
124
+ }
125
+ const filePath = resolvedFile.resolvedTarget;
121
126
  if (fs.existsSync(filePath)) {
122
127
  fs.unlinkSync(filePath);
123
128
  }
@@ -270,10 +275,13 @@ module.exports = {
270
275
  var group = { name: resourcesDirs[i], items: [] };
271
276
  var dirPath = path.resolve(runtime.settings.widgetsFileDir, resourcesDirs[i]);
272
277
  var wwwSubDir = path.join('_widgets', resourcesDirs[i]);
273
- var files = getFiles(dirPath, ['.svg']);
278
+ var files = getFilesRecursive(dirPath, ['.svg']);
274
279
  for (var x = 0; x < files.length; x++) {
275
280
  var filename = files[x];
276
- group.items.push({ path: path.join(wwwSubDir, files[x]).split(path.sep).join(path.posix.sep), name: filename });
281
+ group.items.push({
282
+ path: path.join(wwwSubDir, files[x]).split(path.sep).join(path.posix.sep),
283
+ name: filename
284
+ });
277
285
  }
278
286
  result.groups.push(group);
279
287
  }
@@ -302,7 +310,6 @@ module.exports = {
302
310
  }
303
311
  try {
304
312
  let relPath = req.body?.path;
305
- relPath = relPath.replace(new RegExp('\\.\\.\/', 'g'), '');
306
313
  if (!relPath || typeof relPath !== 'string') {
307
314
  return res.status(400).json({ error: "invalid_path", message: "Missing or invalid widget path." });
308
315
  }
@@ -310,12 +317,12 @@ module.exports = {
310
317
  if (process.versions.electron) {
311
318
  basePath = process.env.userDir || path.join(os.homedir(), '.fuxa');
312
319
  }
313
- const fullPath = path.resolve(basePath, relPath);
314
-
315
- if (!fullPath.startsWith(basePath)) {
316
- runtime.logger.error("api resources/widgets: security_violation " + fullPath);
320
+ const resolvedWidget = resolveWithin(basePath, relPath);
321
+ if (!resolvedWidget) {
322
+ runtime.logger.error("api resources/widgets: security_violation " + relPath);
317
323
  return res.status(403).json({ error: 'security_violation', message: 'Invalid path' });
318
324
  }
325
+ const fullPath = resolvedWidget.resolvedTarget;
319
326
 
320
327
  if (!fs.existsSync(fullPath)) {
321
328
  return res.status(404).json({ error: "not_found", message: "Widget file not found." });
@@ -354,3 +361,17 @@ function getFiles(pathDir, extensions) {
354
361
  .filter((item) => extensions.indexOf(path.extname(item).toLowerCase()) !== -1);
355
362
  return filesInDIrectory;
356
363
  }
364
+
365
+ function getFilesRecursive(pathDir, extensions, baseDir = pathDir) {
366
+ const entries = fs.readdirSync(pathDir, { withFileTypes: true });
367
+ const files = [];
368
+ for (const entry of entries) {
369
+ const entryPath = path.join(pathDir, entry.name);
370
+ if (entry.isDirectory()) {
371
+ files.push(...getFilesRecursive(entryPath, extensions, baseDir));
372
+ } else if (extensions.indexOf(path.extname(entry.name).toLowerCase()) !== -1) {
373
+ files.push(path.relative(baseDir, entryPath));
374
+ }
375
+ }
376
+ return files;
377
+ }
@@ -51,6 +51,16 @@ module.exports = {
51
51
 
52
52
  // POST scheduler data
53
53
  schedulerApp.post("/api/scheduler", secureFnc, function(req, res) {
54
+ if (res.statusCode === 403) {
55
+ runtime.logger.error("api post scheduler: Tocken Expired");
56
+ return;
57
+ }
58
+ const isGuest = authJwt.isGuestUser(req.userId, req.userGroups);
59
+ if (runtime.settings?.secureEnabled && isGuest) {
60
+ res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
61
+ runtime.logger.error("api post scheduler: Unauthorized guest");
62
+ return;
63
+ }
54
64
  try {
55
65
  if (req.body && req.body.id && req.body.data !== undefined) {
56
66
  var schedulerId = req.body.id;
@@ -90,6 +100,16 @@ module.exports = {
90
100
 
91
101
  // DELETE scheduler data
92
102
  schedulerApp.delete("/api/scheduler", secureFnc, function(req, res) {
103
+ if (res.statusCode === 403) {
104
+ runtime.logger.error("api delete scheduler: Tocken Expired");
105
+ return;
106
+ }
107
+ const isGuest = authJwt.isGuestUser(req.userId, req.userGroups);
108
+ if (runtime.settings?.secureEnabled && isGuest) {
109
+ res.status(401).json({error:"unauthorized_error", message: "Unauthorized!"});
110
+ runtime.logger.error("api delete scheduler: Unauthorized guest");
111
+ return;
112
+ }
93
113
  try {
94
114
  if (req.query && req.query.id) {
95
115
  var schedulerId = req.query.id;
@@ -181,4 +201,4 @@ function validateSchedulerData(data) {
181
201
  }
182
202
 
183
203
  return { valid: true };
184
- }
204
+ }