Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
199 lines (176 loc) · 5.97 KB
/
Copy pathserver.js
File metadata and controls
199 lines (176 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');
// 获取本地IP地址
function getLocalIP() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === 'IPv4' && !iface.internal && name !== 'docker0') {
return iface.address;
}
}
}
return '127.0.0.1';
}
// 确定资源路径
function getResourceDir() {
let resourceDir = __dirname;
const possibleResourceDirs = [
path.join(__dirname),
path.join(__dirname, '..'),
path.join(process.resourcesPath || '', 'app'),
path.join(process.resourcesPath || '', 'app.asar'),
process.cwd(),
];
for (const dir of possibleResourceDirs) {
if (dir && fs.existsSync(dir) && fs.existsSync(path.join(dir, 'index.html'))) {
resourceDir = dir;
break;
}
}
return resourceDir;
}
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.wav': 'audio/wav',
'.mp4': 'video/mp4',
'.woff': 'application/font-woff',
'.ttf': 'application/font-ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'application/font-otf',
'.wasm': 'application/wasm',
};
/**
* 在指定端口启动监听,端口被占用时自动切换到随机端口
* @param {http.Server} server - 服务器实例
* @param {number} preferredPort - 首选端口
* @returns {Promise<number>} 实际监听的端口
*/
function listenWithFallback(server, preferredPort) {
return new Promise((resolve, reject) => {
let settled = false;
const onError = (error) => {
if (settled) return;
if (error.code === 'EADDRINUSE') {
console.log(`端口 ${preferredPort} 已被占用,尝试使用随机端口...`);
// 移除当前监听器,避免 fallback 再次触发 EADDRINUSE 时重复处理
server.removeListener('error', onError);
// 端口传 0 让系统分配一个可用端口
server.listen(0, '0.0.0.0', () => {
if (settled) return;
settled = true;
const actualPort = server.address().port;
console.log(`服务器已启动 (随机端口 ${actualPort})`);
resolve(actualPort);
});
// fallback 也可能失败(如权限不足),挂一次性 error 监听器
server.once('error', (err) => {
if (settled) return;
settled = true;
reject(err);
});
} else {
settled = true;
reject(error);
}
};
server.on('error', onError);
server.listen(preferredPort, '0.0.0.0', () => {
if (settled) return;
settled = true;
const actualPort = server.address().port;
resolve(actualPort);
});
});
}
/**
* 启动 HTTP 服务器
* @param {number} port - 端口号,默认 12580
* @returns {Promise<{server: http.Server, port: number, localIP: string}>}
*/
function startServer(port = 12580) {
const localIP = getLocalIP();
const resourceDir = getResourceDir();
console.log('服务器资源目录:', resourceDir);
const server = http.createServer((req, res) => {
const sanitizedPath = path.normalize(req.url).replace(/^([.][/.])+/, '').replace(/\\/g, '/');
let filePath = path.join(resourceDir, sanitizedPath);
if (sanitizedPath === '/' || sanitizedPath === '') {
filePath = path.join(resourceDir, 'index.html');
}
// 路径穿越防御:确保最终路径仍在资源目录内
const resolvedRoot = path.resolve(resourceDir);
const resolvedFile = path.resolve(filePath);
if (resolvedFile !== resolvedRoot && !resolvedFile.startsWith(resolvedRoot + path.sep)) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('403 Forbidden');
return;
}
const extname = String(path.extname(filePath)).toLowerCase();
const contentType = mimeTypes[extname] || 'application/octet-stream';
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code == 'ENOENT') {
const indexPath = path.join(resourceDir, 'index.html');
fs.readFile(indexPath, (indexPathErr, indexContent) => {
if (indexPathErr) {
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end('<h1>404: Page not found</h1>', 'utf-8');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(indexContent, 'utf-8');
}
});
} else {
console.error('服务器读取文件错误:', error);
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error\n');
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
return listenWithFallback(server, port).then((actualPort) => {
console.log('\n====================================');
console.log('🚀 二维码生成器服务器已启动');
console.log('====================================');
console.log(`🌐 本地访问地址: http://localhost:${actualPort}`);
console.log(`🌐 局域网访问地址: http://${localIP}:${actualPort}`);
console.log('====================================\n');
return { server, port: actualPort, localIP };
});
}
/**
* 停止 HTTP 服务器
* @param {http.Server} server - 服务器实例
*/
function stopServer(server) {
if (server) {
server.close();
console.log('🛑 服务器已关闭');
}
}
module.exports = { startServer, stopServer, getLocalIP };
// 独立运行时自动启动服务器
if (require.main === module) {
startServer().catch((error) => {
console.error('服务器启动失败:', error);
process.exit(1);
});
process.on('SIGINT', () => {
console.log('\n🛑 服务器正在关闭...');
process.exit(0);
});
}