chattransfer 2.0.12 → 2.0.13

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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" href="/assets/logo-DQUxWwh1.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>ChatTransfer</title>
8
- <script type="module" crossorigin src="/assets/index-B70USaaW.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BZdEqZiK.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Bg5fD0DP.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chattransfer",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "description": "LAN instant messaging and file transfer tool",
5
5
  "main": "server/index.js",
6
6
  "engines": {
package/server/index.js CHANGED
@@ -3,7 +3,7 @@ const net = require('net');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getIPAddress, openDefaultBrowser } = require('./tool');
6
- const { getCachePath, getConfigFile } = require('./config');
6
+ const { getCachePath, getConfigFile, getDefaultCachePath } = require('./config');
7
7
  const registerRoutes = require('./routes/index');
8
8
  const registerSocket = require('./socket/index');
9
9
  const { MessageHistoryService } = require('./services/message-history-service');
@@ -14,6 +14,7 @@ const {
14
14
  usesIndependentDatabaseFile
15
15
  } = require('./utils/runtime-paths');
16
16
  const { resolveStartOptions } = require('./utils/runtime-options');
17
+ const { getSafeStartupCachePath } = require('./utils/cache-path-validation');
17
18
 
18
19
  const DEFAULT_PORT = 55556;
19
20
  const MAX_RETRY = 3;
@@ -22,7 +23,27 @@ const pkg = require('../package.json');
22
23
 
23
24
  function createApp() {
24
25
  const app = require('express')();
25
- registerRoutes(app);
26
+ return app;
27
+ }
28
+
29
+ function createConfiguredApp(cachePath) {
30
+ const app = createApp();
31
+ const safeCachePath = cachePath
32
+ ? { path: path.resolve(cachePath), changed: false }
33
+ : getSafeStartupCachePath({
34
+ appRoot: path.resolve(__dirname, '..'),
35
+ configPath: getConfigFile(),
36
+ configuredPath: getCachePath(),
37
+ dataRoot: path.dirname(getConfigFile()),
38
+ defaultPath: getDefaultCachePath()
39
+ });
40
+
41
+ if (safeCachePath.changed) {
42
+ console.warn(`[Cache] unsafe configured path rejected: ${getCachePath()}`);
43
+ console.warn(`[Cache] fallback to default: ${safeCachePath.path}`);
44
+ }
45
+
46
+ registerRoutes(app, safeCachePath.path);
26
47
  return app;
27
48
  }
28
49
 
@@ -72,7 +93,22 @@ async function start(options = {}) {
72
93
  return;
73
94
  }
74
95
 
75
- const uploadsDir = getCachePath();
96
+ const configuredCachePath = getCachePath();
97
+ const defaultCachePath = getDefaultCachePath();
98
+ const safeCachePath = getSafeStartupCachePath({
99
+ appRoot: path.resolve(__dirname, '..'),
100
+ configPath: getConfigFile(),
101
+ configuredPath: configuredCachePath,
102
+ dataRoot: path.dirname(getConfigFile()),
103
+ defaultPath: defaultCachePath
104
+ });
105
+
106
+ if (safeCachePath.changed) {
107
+ console.warn(`[Cache] unsafe configured path rejected: ${configuredCachePath}`);
108
+ console.warn(`[Cache] fallback to default: ${safeCachePath.path}`);
109
+ }
110
+
111
+ const uploadsDir = safeCachePath.path;
76
112
  await fs.promises.mkdir(uploadsDir, { recursive: true });
77
113
 
78
114
  const historyService = new MessageHistoryService({
@@ -87,7 +123,7 @@ async function start(options = {}) {
87
123
  return;
88
124
  }
89
125
 
90
- const app = createApp();
126
+ const app = createConfiguredApp(uploadsDir);
91
127
  const server = http.createServer(app);
92
128
 
93
129
  registerSocket(server, historyService);
@@ -127,6 +163,6 @@ if (require.main === module) {
127
163
  }
128
164
 
129
165
  module.exports = {
130
- createApp,
166
+ createApp: createConfiguredApp,
131
167
  start
132
168
  };
@@ -1,49 +1,52 @@
1
1
  const express = require('express');
2
2
  const path = require('path');
3
- const router = express.Router();
4
3
  const { getCachePath } = require('../config');
5
4
 
6
5
  function sanitizeFileName(name) {
7
6
  return path.basename(name || '').replace(/[\\/:*?"<>|\u0000]/g, '_');
8
7
  }
9
8
 
10
- router.post('/uploadFile', async (req, res) => {
11
- const uploadsDir = getCachePath();
12
- const files = req.files && (req.files.file_data || req.files.fileObj);
13
-
14
- if (!files) {
15
- return res.status(400).send({
16
- code: -1,
17
- message: 'No file uploaded'
18
- });
19
- }
20
-
21
- const fileList = Array.isArray(files) ? files : [files];
22
- const uploadResults = [];
23
-
24
- for (const file of fileList) {
25
- const timestamp = Date.now();
26
- const safeName = `${timestamp}_${sanitizeFileName(file.name)}`;
27
- const filePath = path.join(uploadsDir, 'files', safeName);
28
-
29
- await new Promise((resolve, reject) => {
30
- file.mv(filePath, (error) => (error ? reject(error) : resolve()));
31
- });
32
-
33
- uploadResults.push({
34
- name: safeName,
35
- mimetype: file.mimetype,
36
- size: file.size,
37
- fileUrl: `${req.protocol}://${req.headers.host}/files/${encodeURIComponent(safeName)}`,
38
- localFilePath: filePath
9
+ module.exports = (configuredUploadsDir = getCachePath()) => {
10
+ const router = express.Router();
11
+ const uploadsDir = path.resolve(configuredUploadsDir);
12
+
13
+ router.post('/uploadFile', async (req, res) => {
14
+ const files = req.files && (req.files.file_data || req.files.fileObj);
15
+
16
+ if (!files) {
17
+ return res.status(400).send({
18
+ code: -1,
19
+ message: 'No file uploaded'
20
+ });
21
+ }
22
+
23
+ const fileList = Array.isArray(files) ? files : [files];
24
+ const uploadResults = [];
25
+
26
+ for (const file of fileList) {
27
+ const timestamp = Date.now();
28
+ const safeName = `${timestamp}_${sanitizeFileName(file.name)}`;
29
+ const filePath = path.join(uploadsDir, 'files', safeName);
30
+
31
+ await new Promise((resolve, reject) => {
32
+ file.mv(filePath, (error) => (error ? reject(error) : resolve()));
33
+ });
34
+
35
+ uploadResults.push({
36
+ name: safeName,
37
+ mimetype: file.mimetype,
38
+ size: file.size,
39
+ fileUrl: `${req.protocol}://${req.headers.host}/files/${encodeURIComponent(safeName)}`,
40
+ localFilePath: filePath
41
+ });
42
+ }
43
+
44
+ res.send({
45
+ code: 0,
46
+ message: fileList.length > 1 ? 'Files uploaded' : 'File uploaded',
47
+ data: fileList.length > 1 ? uploadResults : uploadResults[0]
39
48
  });
40
- }
41
-
42
- res.send({
43
- code: 0,
44
- message: fileList.length > 1 ? 'Files uploaded' : 'File uploaded',
45
- data: fileList.length > 1 ? uploadResults : uploadResults[0]
46
49
  });
47
- });
48
50
 
49
- module.exports = router;
51
+ return router;
52
+ };
@@ -5,10 +5,15 @@ const fs = require('fs');
5
5
  const fileUpload = require('express-fileupload');
6
6
  const { getCachePath } = require('../config');
7
7
 
8
- module.exports = function registerRoutes(app) {
9
- const uploadsDir = getCachePath();
8
+ module.exports = function registerRoutes(app, configuredUploadsDir = getCachePath()) {
9
+ const uploadsDir = path.resolve(configuredUploadsDir);
10
+ const filesDir = path.join(uploadsDir, 'files');
10
11
  const tempDir = path.join(uploadsDir, 'tmp');
11
12
 
13
+ if (!fs.existsSync(filesDir)) {
14
+ fs.mkdirSync(filesDir, { recursive: true });
15
+ }
16
+
12
17
  if (!fs.existsSync(tempDir)) {
13
18
  fs.mkdirSync(tempDir, { recursive: true });
14
19
  }
@@ -22,13 +27,13 @@ module.exports = function registerRoutes(app) {
22
27
 
23
28
  app.use(express.json());
24
29
 
25
- app.use(require('./system'));
30
+ app.use(require('./system')(uploadsDir));
26
31
  app.use(require('./session'));
27
32
  app.use(require('./qr'));
28
- app.use(require('./files'));
33
+ app.use(require('./files')(uploadsDir));
29
34
 
30
35
  app.use(express.static(path.join(__dirname, '../../frontend/dist')));
31
- app.use(express.static(uploadsDir));
36
+ app.use('/files', express.static(filesDir));
32
37
 
33
38
  app.get('*', (req, res) => {
34
39
  res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
@@ -1,107 +1,119 @@
1
1
  const express = require('express');
2
2
  const path = require('path');
3
+ const fs = require('fs');
3
4
  const { execFile } = require('child_process');
4
- const router = express.Router();
5
5
  const {
6
6
  getCachePath,
7
7
  getConfigFile,
8
8
  getDefaultCachePath,
9
- isCustomized,
10
9
  setCachePath
11
10
  } = require('../config');
12
11
  const { getDefaultDataDir } = require('../utils/runtime-paths');
12
+ const { validateCachePath } = require('../utils/cache-path-validation');
13
13
  const { trackEvent } = require('../telemetry/compat');
14
14
 
15
- router.get('/getHomeDir', (req, res) => {
16
- const uploadsDir = getCachePath();
17
-
18
- res.send({
19
- code: 0,
20
- message: 'ok',
21
- data: {
22
- uploadsDir,
23
- defaultDir: getDefaultCachePath(),
24
- isCustomized: isCustomized(),
25
- configFile: getConfigFile()
26
- }
27
- });
28
- });
29
-
30
- router.get('/getVersionInfo', (req, res) => {
31
- res.send({
32
- code: 0,
33
- message: 'ok',
34
- data: {
35
- version: require('../../package.json').version,
36
- deviceId: 'anonymous'
37
- }
38
- });
39
- });
40
-
41
- router.post('/openCachePath', express.json(), (req, res) => {
42
- const uploadsDir = getCachePath();
43
- const respond = (code, message) => res.send({
44
- code,
45
- message,
46
- data: { platform: process.platform, path: uploadsDir }
47
- });
48
-
49
- if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
50
- respond(0, '服务器无桌面环境,缓存目录已返回');
51
- return;
52
- }
15
+ module.exports = (configuredUploadsDir = getCachePath()) => {
16
+ const router = express.Router();
17
+ const uploadsDir = path.resolve(configuredUploadsDir);
53
18
 
54
- if (process.platform === 'darwin') {
55
- execFile('open', [uploadsDir], (error) => {
56
- respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
19
+ router.get('/getHomeDir', (req, res) => {
20
+ res.send({
21
+ code: 0,
22
+ message: 'ok',
23
+ data: {
24
+ uploadsDir,
25
+ defaultDir: getDefaultCachePath(),
26
+ isCustomized: uploadsDir !== path.resolve(getDefaultCachePath()),
27
+ configFile: getConfigFile()
28
+ }
57
29
  });
58
- } else if (process.platform === 'win32') {
59
- execFile('explorer', [uploadsDir], (error) => {
60
- respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
30
+ });
31
+
32
+ router.get('/getVersionInfo', (req, res) => {
33
+ res.send({
34
+ code: 0,
35
+ message: 'ok',
36
+ data: {
37
+ version: require('../../package.json').version,
38
+ deviceId: 'anonymous'
39
+ }
61
40
  });
62
- } else {
63
- execFile('xdg-open', [uploadsDir], (error) => {
64
- respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
41
+ });
42
+
43
+ router.post('/openCachePath', express.json(), (req, res) => {
44
+ const respond = (code, message) => res.send({
45
+ code,
46
+ message,
47
+ data: { platform: process.platform, path: uploadsDir }
65
48
  });
66
- }
67
- });
68
49
 
69
- router.post('/setCachePath', (req, res) => {
70
- const inputPath = req.body && req.body.path;
50
+ if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
51
+ respond(0, '服务器无桌面环境,缓存目录已返回');
52
+ return;
53
+ }
71
54
 
72
- if (!inputPath || typeof inputPath !== 'string') {
73
- return res.send({ code: -1, message: 'invalid path' });
74
- }
55
+ if (process.platform === 'darwin') {
56
+ execFile('open', [uploadsDir], (error) => {
57
+ respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
58
+ });
59
+ } else if (process.platform === 'win32') {
60
+ execFile('explorer', [uploadsDir], (error) => {
61
+ respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
62
+ });
63
+ } else {
64
+ execFile('xdg-open', [uploadsDir], (error) => {
65
+ respond(error ? -1 : 0, error ? `打开失败:${error.message}` : '打开成功');
66
+ });
67
+ }
68
+ });
75
69
 
76
- const newPath = path.resolve(inputPath.trim());
77
- const dataRoot = path.resolve(getDefaultDataDir());
70
+ router.post('/setCachePath', (req, res) => {
71
+ const inputPath = req.body && req.body.path;
78
72
 
79
- if (!path.isAbsolute(newPath) || newPath === path.resolve(getCachePath()) || newPath === dataRoot) {
80
- return res.send({ code: -1, message: 'invalid cache path' });
81
- }
73
+ if (!inputPath || typeof inputPath !== 'string') {
74
+ return res.send({ code: -1, message: 'invalid path' });
75
+ }
82
76
 
83
- try {
84
- require('fs').mkdirSync(newPath, { recursive: true });
85
- setCachePath(newPath);
86
- res.send({
87
- code: 0,
88
- message: 'ok',
89
- data: { newPath, needsRestart: true }
77
+ const validation = validateCachePath(inputPath, {
78
+ appRoot: path.resolve(__dirname, '..', '..'),
79
+ configPath: getConfigFile(),
80
+ currentPath: uploadsDir,
81
+ dataRoot: getDefaultDataDir()
90
82
  });
91
- } catch (error) {
92
- res.send({ code: -1, message: error.message });
93
- }
94
- });
95
83
 
96
- router.post('/api/track', (req, res) => {
97
- const { eventType, eventProperties, userId } = req.body || {};
84
+ if (!validation.valid) {
85
+ return res.send({ code: -1, message: 'invalid cache path' });
86
+ }
87
+
88
+ try {
89
+ const newPath = validation.path;
90
+ fs.mkdirSync(newPath, { recursive: true });
91
+
92
+ const testFile = path.join(newPath, '.chattransfer-write-test');
93
+ fs.writeFileSync(testFile, 'ok', 'utf8');
94
+ fs.unlinkSync(testFile);
95
+
96
+ setCachePath(newPath);
97
+ res.send({
98
+ code: 0,
99
+ message: 'ok',
100
+ data: { newPath, needsRestart: true }
101
+ });
102
+ } catch (error) {
103
+ res.send({ code: -1, message: error.message });
104
+ }
105
+ });
106
+
107
+ router.post('/api/track', (req, res) => {
108
+ const { eventType, eventProperties, userId } = req.body || {};
98
109
 
99
- if (!eventType) {
100
- return res.status(400).send({ code: -1, message: 'eventType is required' });
101
- }
110
+ if (!eventType) {
111
+ return res.status(400).send({ code: -1, message: 'eventType is required' });
112
+ }
102
113
 
103
- trackEvent(eventType, eventProperties || {}, userId || null);
104
- res.send({ code: 0, message: 'ok' });
105
- });
114
+ trackEvent(eventType, eventProperties || {}, userId || null);
115
+ res.send({ code: 0, message: 'ok' });
116
+ });
106
117
 
107
- module.exports = router;
118
+ return router;
119
+ };
@@ -0,0 +1,125 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ function isWithin(parent, child) {
6
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
7
+ return relative === '' || (
8
+ relative !== '..' &&
9
+ !relative.startsWith(`..${path.sep}`) &&
10
+ !path.isAbsolute(relative)
11
+ );
12
+ }
13
+
14
+ function getSystemRoots() {
15
+ if (process.platform === 'win32') {
16
+ return [
17
+ process.env.SystemRoot,
18
+ process.env.ProgramFiles,
19
+ process.env['ProgramFiles(x86)'],
20
+ process.env.ProgramData
21
+ ].filter(Boolean);
22
+ }
23
+
24
+ return [
25
+ '/bin',
26
+ '/boot',
27
+ '/dev',
28
+ '/etc',
29
+ '/lib',
30
+ '/lib64',
31
+ '/proc',
32
+ '/run',
33
+ '/sbin',
34
+ '/sys',
35
+ '/usr',
36
+ '/var'
37
+ ];
38
+ }
39
+
40
+ function resolvePathForValidation(filePath) {
41
+ const resolved = path.resolve(filePath);
42
+ let nearest = resolved;
43
+
44
+ while (!fs.existsSync(nearest)) {
45
+ const parent = path.dirname(nearest);
46
+ if (parent === nearest) break;
47
+ nearest = parent;
48
+ }
49
+
50
+ const nearestRealPath = fs.realpathSync(nearest);
51
+ if (nearest === resolved) return nearestRealPath;
52
+
53
+ return path.resolve(nearestRealPath, path.relative(nearest, resolved));
54
+ }
55
+
56
+ function validateCachePath(inputPath, {
57
+ appRoot,
58
+ configPath,
59
+ currentPath,
60
+ dataRoot,
61
+ homeDir = os.homedir()
62
+ } = {}) {
63
+ if (typeof inputPath !== 'string' || !inputPath.trim()) {
64
+ return { valid: false, reason: 'empty' };
65
+ }
66
+
67
+ if (!path.isAbsolute(inputPath.trim())) {
68
+ return { valid: false, reason: 'relative' };
69
+ }
70
+
71
+ const newPath = path.resolve(inputPath.trim());
72
+ const realPath = resolvePathForValidation(newPath);
73
+ const filesystemRoot = path.parse(realPath).root;
74
+ const systemRoots = getSystemRoots().map((item) => resolvePathForValidation(item));
75
+ const protectedPaths = [
76
+ filesystemRoot,
77
+ appRoot,
78
+ configPath && path.dirname(configPath),
79
+ currentPath,
80
+ dataRoot,
81
+ homeDir,
82
+ process.cwd(),
83
+ ...systemRoots
84
+ ].filter(Boolean).map((item) => path.resolve(item));
85
+
86
+ if (protectedPaths.some((protectedPath) => isWithin(realPath, protectedPath))) {
87
+ return { valid: false, reason: 'protected' };
88
+ }
89
+
90
+ return { valid: true, path: newPath, realPath };
91
+ }
92
+
93
+ function getSafeStartupCachePath({
94
+ appRoot,
95
+ configPath,
96
+ configuredPath,
97
+ dataRoot,
98
+ defaultPath
99
+ }) {
100
+ if (configuredPath === defaultPath) {
101
+ return { path: defaultPath, changed: false };
102
+ }
103
+
104
+ const validation = validateCachePath(configuredPath, {
105
+ appRoot,
106
+ configPath,
107
+ dataRoot
108
+ });
109
+
110
+ if (validation.valid) {
111
+ return { path: validation.path, changed: false };
112
+ }
113
+
114
+ return {
115
+ path: defaultPath,
116
+ changed: true,
117
+ reason: validation.reason
118
+ };
119
+ }
120
+
121
+ module.exports = {
122
+ getSafeStartupCachePath,
123
+ isWithin,
124
+ validateCachePath
125
+ };
@@ -1,94 +0,0 @@
1
- /**
2
- * 设置自定义缓存目录
3
- * 请求体: { path: string, migrate?: boolean }
4
- * 生效方式: 修改后需重启进程(express.static / fileUpload 中间件运行时不可热切换)
5
- * @param {*} app
6
- * @param {*} uploadsDir 当前生效缓存目录
7
- */
8
- const fs = require('fs');
9
- const path = require('path');
10
- const express = require('express');
11
- const { setCachePath } = require('../config.js');
12
- const { getAppWritableRoot } = require('../tool.js');
13
-
14
- module.exports = (app, uploadsDir) => {
15
- app.post('/setCachePath', express.json(), async (req, res) => {
16
- try {
17
- const inputPath = req.body && req.body.path;
18
- const migrate = req.body ? req.body.migrate !== false : true;
19
-
20
- // 1. 参数校验
21
- if (!inputPath || typeof inputPath !== 'string' || !inputPath.trim()) {
22
- return res.send({ code: -1, message: '请输入缓存目录路径' });
23
- }
24
-
25
- // 2. 必须绝对路径(先校验原始输入,再 resolve 规范化)
26
- const trimmed = inputPath.trim();
27
- if (!path.isAbsolute(trimmed)) {
28
- return res.send({ code: -1, message: '请输入绝对路径' });
29
- }
30
- const newPath = path.resolve(trimmed);
31
-
32
- // 3. 不能等于当前路径
33
- if (newPath === path.resolve(uploadsDir)) {
34
- return res.send({ code: -1, message: '新路径与当前路径相同' });
35
- }
36
-
37
- // 4. 不能等于应用根目录(避免污染应用文件)
38
- const appRoot = getAppWritableRoot();
39
- if (newPath === path.resolve(appRoot)) {
40
- return res.send({ code: -1, message: '不能使用应用根目录作为缓存目录' });
41
- }
42
-
43
- // 5. 目录不存在则创建
44
- try {
45
- if (!fs.existsSync(newPath)) {
46
- fs.mkdirSync(newPath, { recursive: true });
47
- }
48
- } catch (e) {
49
- return res.send({ code: -1, message: '目录创建失败:' + e.message });
50
- }
51
-
52
- // 6. 可写性测试:写临时文件再删
53
- const testFile = path.join(newPath, '.chattransfer-write-test');
54
- try {
55
- fs.writeFileSync(testFile, 'ok', 'utf8');
56
- fs.unlinkSync(testFile);
57
- } catch (e) {
58
- return res.send({ code: -1, message: '路径无效或不可写' });
59
- }
60
-
61
- // 7. 迁移已有文件(递归复制旧目录内容到新目录)
62
- if (migrate && fs.existsSync(uploadsDir)) {
63
- try {
64
- fs.cpSync(uploadsDir, newPath, { recursive: true });
65
- // 清理测试遗留(若有)
66
- if (fs.existsSync(testFile)) fs.unlinkSync(testFile);
67
- } catch (e) {
68
- // 迁移失败:清理已复制的内容,回滚,不更新配置
69
- console.error('[setCachePath] migrate failed:', e.message);
70
- return res.send({ code: -1, message: '迁移文件失败:' + e.message });
71
- }
72
- }
73
-
74
- // 8. 持久化到 config.json
75
- try {
76
- setCachePath(newPath);
77
- } catch (e) {
78
- return res.send({ code: -1, message: '配置保存失败:' + e.message });
79
- }
80
-
81
- res.send({
82
- code: 0,
83
- message: '缓存目录设置成功,重启应用后生效',
84
- data: {
85
- newPath: newPath,
86
- needsRestart: true
87
- }
88
- });
89
- } catch (err) {
90
- console.log(err);
91
- res.status(500).send({ code: -1, message: err.message || '服务器内部错误' });
92
- }
93
- });
94
- }