@chenpu17/serve-here 1.0.0 → 1.0.2
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/README.md +2 -0
- package/bin/serve-here.js +262 -2
- package/package.json +1 -1
- package/src/server.js +164 -93
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Serve any local directory over HTTP with a single command.
|
|
|
8
8
|
立即托管当前目录或指定目录。
|
|
9
9
|
- Automatic `index.html` support plus clean directory listings.
|
|
10
10
|
自动识别 `index.html`,无首页时提供整洁的目录列表。
|
|
11
|
+
- Safe redirects for non-ASCII directory names (e.g. 中文路径).
|
|
12
|
+
支持中文等非 ASCII 路径的安全重定向。
|
|
11
13
|
- Directory rows include file size and last modified time.
|
|
12
14
|
列表项展示文件大小与最近修改时间。
|
|
13
15
|
- Works as a global install or `npx` one-off.
|
package/bin/serve-here.js
CHANGED
|
@@ -4,12 +4,212 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const process = require('process');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
7
8
|
|
|
8
9
|
const { createStaticServer } = require('../src/server');
|
|
9
10
|
const pkg = require('../package.json');
|
|
10
11
|
|
|
11
12
|
const DEFAULT_PORT = 8080;
|
|
12
13
|
const DEFAULT_HOST = '0.0.0.0';
|
|
14
|
+
const PID_DIR = path.join(os.homedir(), '.serve-here');
|
|
15
|
+
const LOG_DIR = path.join(os.homedir(), '.serve-here', 'logs');
|
|
16
|
+
|
|
17
|
+
function ensureDirectories() {
|
|
18
|
+
if (!fs.existsSync(PID_DIR)) {
|
|
19
|
+
fs.mkdirSync(PID_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
22
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getPidFile(port) {
|
|
27
|
+
return path.join(PID_DIR, `serve-here-${port}.pid`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getLogFile(port) {
|
|
31
|
+
return path.join(LOG_DIR, `serve-here-${port}.log`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readPidFile(port) {
|
|
35
|
+
const pidFile = getPidFile(port);
|
|
36
|
+
if (fs.existsSync(pidFile)) {
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(pidFile, 'utf8').trim();
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const pid = parseInt(lines[0], 10);
|
|
41
|
+
const rootDir = lines[1] || '';
|
|
42
|
+
return { pid, rootDir };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writePidFile(port, pid, rootDir) {
|
|
51
|
+
const pidFile = getPidFile(port);
|
|
52
|
+
fs.writeFileSync(pidFile, `${pid}\n${rootDir}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function removePidFile(port) {
|
|
56
|
+
const pidFile = getPidFile(port);
|
|
57
|
+
if (fs.existsSync(pidFile)) {
|
|
58
|
+
fs.unlinkSync(pidFile);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isProcessRunning(pid) {
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0);
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startDaemon(options) {
|
|
72
|
+
ensureDirectories();
|
|
73
|
+
|
|
74
|
+
const rootDir = path.resolve(options.directory || process.cwd());
|
|
75
|
+
const port = options.port || DEFAULT_PORT;
|
|
76
|
+
const host = options.host || DEFAULT_HOST;
|
|
77
|
+
|
|
78
|
+
// Check if already running
|
|
79
|
+
const pidInfo = readPidFile(port);
|
|
80
|
+
if (pidInfo && isProcessRunning(pidInfo.pid)) {
|
|
81
|
+
console.error(`Error: Server already running on port ${port} (PID: ${pidInfo.pid})`);
|
|
82
|
+
console.error(`Serving: ${pidInfo.rootDir}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate directory
|
|
87
|
+
let stats;
|
|
88
|
+
try {
|
|
89
|
+
stats = fs.statSync(rootDir);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`Error: directory "${rootDir}" does not exist or is not accessible.`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (!stats.isDirectory()) {
|
|
95
|
+
console.error(`Error: path "${rootDir}" is not a directory.`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const logFile = getLogFile(port);
|
|
100
|
+
const out = fs.openSync(logFile, 'a');
|
|
101
|
+
const err = fs.openSync(logFile, 'a');
|
|
102
|
+
|
|
103
|
+
const child = spawn(process.execPath, [__filename, '--daemon-child', '-d', rootDir, '-p', String(port), '-H', host], {
|
|
104
|
+
detached: true,
|
|
105
|
+
stdio: ['ignore', out, err],
|
|
106
|
+
env: process.env
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
child.unref();
|
|
110
|
+
|
|
111
|
+
writePidFile(port, child.pid, rootDir);
|
|
112
|
+
|
|
113
|
+
console.log(`Server started in background (PID: ${child.pid})`);
|
|
114
|
+
console.log(`Serving: ${rootDir}`);
|
|
115
|
+
console.log(`Listening on port: ${port}`);
|
|
116
|
+
console.log(`Log file: ${logFile}`);
|
|
117
|
+
console.log(`\nTo stop: serve-here --stop -p ${port}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stopDaemon(port) {
|
|
121
|
+
const pidInfo = readPidFile(port);
|
|
122
|
+
if (!pidInfo) {
|
|
123
|
+
console.error(`No server running on port ${port}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!isProcessRunning(pidInfo.pid)) {
|
|
128
|
+
console.log(`Server (PID: ${pidInfo.pid}) is not running, cleaning up...`);
|
|
129
|
+
removePidFile(port);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
process.kill(pidInfo.pid, 'SIGTERM');
|
|
135
|
+
console.log(`Stopping server (PID: ${pidInfo.pid})...`);
|
|
136
|
+
|
|
137
|
+
// Wait for process to stop
|
|
138
|
+
let attempts = 0;
|
|
139
|
+
const checkInterval = setInterval(() => {
|
|
140
|
+
attempts++;
|
|
141
|
+
if (!isProcessRunning(pidInfo.pid)) {
|
|
142
|
+
clearInterval(checkInterval);
|
|
143
|
+
removePidFile(port);
|
|
144
|
+
console.log('Server stopped.');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
} else if (attempts >= 10) {
|
|
147
|
+
clearInterval(checkInterval);
|
|
148
|
+
console.error('Server did not stop gracefully, force killing...');
|
|
149
|
+
try {
|
|
150
|
+
process.kill(pidInfo.pid, 'SIGKILL');
|
|
151
|
+
} catch (e) {}
|
|
152
|
+
removePidFile(port);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
}, 500);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`Failed to stop server: ${error.message}`);
|
|
158
|
+
removePidFile(port);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function showStatus(port) {
|
|
164
|
+
if (port) {
|
|
165
|
+
const pidInfo = readPidFile(port);
|
|
166
|
+
if (!pidInfo) {
|
|
167
|
+
console.log(`No server running on port ${port}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const running = isProcessRunning(pidInfo.pid);
|
|
172
|
+
console.log(`Port ${port}:`);
|
|
173
|
+
console.log(` PID: ${pidInfo.pid}`);
|
|
174
|
+
console.log(` Status: ${running ? 'running' : 'stopped'}`);
|
|
175
|
+
console.log(` Directory: ${pidInfo.rootDir}`);
|
|
176
|
+
console.log(` Log: ${getLogFile(port)}`);
|
|
177
|
+
|
|
178
|
+
if (!running) {
|
|
179
|
+
removePidFile(port);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Show all
|
|
183
|
+
ensureDirectories();
|
|
184
|
+
const files = fs.readdirSync(PID_DIR).filter(f => f.endsWith('.pid'));
|
|
185
|
+
|
|
186
|
+
if (files.length === 0) {
|
|
187
|
+
console.log('No servers running.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('Running servers:\n');
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const match = file.match(/serve-here-(\d+)\.pid/);
|
|
194
|
+
if (match) {
|
|
195
|
+
const p = parseInt(match[1], 10);
|
|
196
|
+
const pidInfo = readPidFile(p);
|
|
197
|
+
if (pidInfo) {
|
|
198
|
+
const running = isProcessRunning(pidInfo.pid);
|
|
199
|
+
console.log(`Port ${p}:`);
|
|
200
|
+
console.log(` PID: ${pidInfo.pid}`);
|
|
201
|
+
console.log(` Status: ${running ? 'running' : 'stopped'}`);
|
|
202
|
+
console.log(` Directory: ${pidInfo.rootDir}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
if (!running) {
|
|
206
|
+
removePidFile(p);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
13
213
|
|
|
14
214
|
function main() {
|
|
15
215
|
try {
|
|
@@ -25,6 +225,22 @@ function main() {
|
|
|
25
225
|
process.exit(0);
|
|
26
226
|
}
|
|
27
227
|
|
|
228
|
+
// Handle daemon commands
|
|
229
|
+
if (options.stop) {
|
|
230
|
+
stopDaemon(options.port || DEFAULT_PORT);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (options.status) {
|
|
235
|
+
showStatus(options.port);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (options.daemon && !options.daemonChild) {
|
|
240
|
+
startDaemon(options);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
28
244
|
const rootDir = path.resolve(options.directory || process.cwd());
|
|
29
245
|
|
|
30
246
|
let stats;
|
|
@@ -44,8 +260,16 @@ function main() {
|
|
|
44
260
|
const port = options.port || DEFAULT_PORT;
|
|
45
261
|
const host = options.host || DEFAULT_HOST;
|
|
46
262
|
|
|
263
|
+
if (options.daemonChild) {
|
|
264
|
+
ensureDirectories();
|
|
265
|
+
process.on('exit', () => removePidFile(port));
|
|
266
|
+
}
|
|
267
|
+
|
|
47
268
|
server.on('error', error => {
|
|
48
269
|
console.error('Failed to start server:', error.message);
|
|
270
|
+
if (options.daemonChild) {
|
|
271
|
+
removePidFile(port);
|
|
272
|
+
}
|
|
49
273
|
process.exit(1);
|
|
50
274
|
});
|
|
51
275
|
|
|
@@ -56,10 +280,18 @@ function main() {
|
|
|
56
280
|
...formatListeningAddresses(host, port)
|
|
57
281
|
];
|
|
58
282
|
console.log(messageLines.join('\n '));
|
|
283
|
+
|
|
284
|
+
// Write PID file when running as daemon child
|
|
285
|
+
if (options.daemonChild) {
|
|
286
|
+
writePidFile(port, process.pid, rootDir);
|
|
287
|
+
}
|
|
59
288
|
});
|
|
60
289
|
|
|
61
290
|
const shutdown = () => {
|
|
62
291
|
console.log('\nShutting down...');
|
|
292
|
+
if (options.daemonChild) {
|
|
293
|
+
removePidFile(port);
|
|
294
|
+
}
|
|
63
295
|
server.close(() => process.exit(0));
|
|
64
296
|
};
|
|
65
297
|
|
|
@@ -77,7 +309,11 @@ function parseArguments(args) {
|
|
|
77
309
|
port: undefined,
|
|
78
310
|
host: undefined,
|
|
79
311
|
help: false,
|
|
80
|
-
version: false
|
|
312
|
+
version: false,
|
|
313
|
+
daemon: false,
|
|
314
|
+
daemonChild: false,
|
|
315
|
+
stop: false,
|
|
316
|
+
status: false
|
|
81
317
|
};
|
|
82
318
|
|
|
83
319
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -105,6 +341,19 @@ function parseArguments(args) {
|
|
|
105
341
|
case '--host':
|
|
106
342
|
options.host = requireValue(args, ++i, '--host');
|
|
107
343
|
break;
|
|
344
|
+
case '-D':
|
|
345
|
+
case '--daemon':
|
|
346
|
+
options.daemon = true;
|
|
347
|
+
break;
|
|
348
|
+
case '--daemon-child':
|
|
349
|
+
options.daemonChild = true;
|
|
350
|
+
break;
|
|
351
|
+
case '--stop':
|
|
352
|
+
options.stop = true;
|
|
353
|
+
break;
|
|
354
|
+
case '--status':
|
|
355
|
+
options.status = true;
|
|
356
|
+
break;
|
|
108
357
|
default:
|
|
109
358
|
if (arg.startsWith('-')) {
|
|
110
359
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -167,8 +416,19 @@ Options:
|
|
|
167
416
|
-d, --dir <path> Directory to serve (defaults to current working directory)
|
|
168
417
|
-p, --port <number> Port to listen on (default: ${DEFAULT_PORT})
|
|
169
418
|
-H, --host <address> Hostname or IP to bind (default: ${DEFAULT_HOST})
|
|
419
|
+
-D, --daemon Run as a background daemon (does not occupy terminal)
|
|
420
|
+
--stop Stop a running daemon (use with -p to specify port)
|
|
421
|
+
--status Show status of running daemon(s)
|
|
170
422
|
-h, --help Show this help message
|
|
171
|
-
-V, --version Show version
|
|
423
|
+
-V, --version Show version
|
|
424
|
+
|
|
425
|
+
Examples:
|
|
426
|
+
serve-here Start server in foreground on port 8080
|
|
427
|
+
serve-here -D Start server as daemon on port 8080
|
|
428
|
+
serve-here -D -p 3000 Start daemon on port 3000
|
|
429
|
+
serve-here --stop Stop daemon on port 8080
|
|
430
|
+
serve-here --stop -p 3000 Stop daemon on port 3000
|
|
431
|
+
serve-here --status Show all running daemons`);
|
|
172
432
|
}
|
|
173
433
|
|
|
174
434
|
main();
|
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -30,127 +30,162 @@ function createStaticServer({
|
|
|
30
30
|
? resolvedRoot
|
|
31
31
|
: `${resolvedRoot}${path.sep}`;
|
|
32
32
|
|
|
33
|
-
const server = http.createServer(
|
|
33
|
+
const server = http.createServer((req, res) => {
|
|
34
34
|
const startTime = Date.now();
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
42
|
-
res.setHeader('Allow', 'GET, HEAD');
|
|
43
|
-
sendError(res, 405, 'Method Not Allowed');
|
|
44
|
-
logRequest(logger, req, res.statusCode, startTime);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
36
|
+
void (async () => {
|
|
37
|
+
if (!req.url) {
|
|
38
|
+
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
42
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
43
|
+
res.setHeader('Allow', 'GET, HEAD');
|
|
44
|
+
sendError(res, 405, 'Method Not Allowed', false);
|
|
45
|
+
logRequest(logger, req, res.statusCode, startTime);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
let decodedPath;
|
|
50
|
+
let requestPathname;
|
|
51
|
+
let requestSearch;
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(req.url, 'http://localhost');
|
|
54
|
+
requestPathname = url.pathname;
|
|
55
|
+
requestSearch = url.search;
|
|
56
|
+
decodedPath = decodeURIComponent(requestPathname);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
59
|
+
logRequest(logger, req, res.statusCode, startTime, error);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
63
|
+
const candidateSegments = decodedPath
|
|
64
|
+
.split('/')
|
|
65
|
+
.filter(segment => segment && segment !== '.');
|
|
66
|
+
const resolvedPath = path.resolve(resolvedRoot, ...candidateSegments);
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (error.code === 'ENOENT') {
|
|
74
|
-
sendError(res, 404, 'Not Found');
|
|
75
|
-
} else {
|
|
76
|
-
sendError(res, 500, 'Internal Server Error');
|
|
77
|
-
logger.error('Error reading path', resolvedPath, error);
|
|
68
|
+
if (!resolvedPath.startsWith(rootWithSep) && resolvedPath !== resolvedRoot) {
|
|
69
|
+
sendError(res, 403, 'Forbidden', req.method === 'HEAD');
|
|
70
|
+
logRequest(logger, req, res.statusCode, startTime);
|
|
71
|
+
return;
|
|
78
72
|
}
|
|
79
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
let stats;
|
|
75
|
+
try {
|
|
76
|
+
stats = await stat(resolvedPath);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === 'ENOENT') {
|
|
79
|
+
sendError(res, 404, 'Not Found', req.method === 'HEAD');
|
|
80
|
+
} else {
|
|
81
|
+
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
82
|
+
logger.error('Error reading path', resolvedPath, error);
|
|
83
|
+
}
|
|
84
|
+
logRequest(logger, req, res.statusCode, startTime, error);
|
|
90
85
|
return;
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
if (!requestPathname.endsWith('/')) {
|
|
90
|
+
// Align with browser expectations for relative asset loading.
|
|
91
|
+
const location = `${requestPathname}/${requestSearch}`;
|
|
92
|
+
try {
|
|
93
|
+
// Use the encoded pathname for headers (Node rejects non-ASCII in Location).
|
|
94
|
+
res.statusCode = 301;
|
|
95
|
+
res.setHeader('Location', location);
|
|
96
|
+
res.end();
|
|
99
97
|
logRequest(logger, req, res.statusCode, startTime);
|
|
100
98
|
return;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
sendError(res, 400, 'Bad Request', req.method === 'HEAD');
|
|
101
|
+
logRequest(logger, req, res.statusCode, startTime, error);
|
|
102
|
+
return;
|
|
101
103
|
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const indexFile of indexFiles) {
|
|
107
|
+
const candidate = path.join(resolvedPath, indexFile);
|
|
108
|
+
try {
|
|
109
|
+
const indexStats = await stat(candidate);
|
|
110
|
+
if (indexStats.isFile()) {
|
|
111
|
+
await serveFile(res, candidate, indexStats, req.method === 'HEAD');
|
|
112
|
+
logRequest(logger, req, res.statusCode, startTime);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code !== 'ENOENT') {
|
|
117
|
+
logger.error('Error reading index file', candidate, error);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
|
-
}
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
if (!enableDirectoryListing) {
|
|
123
|
+
sendError(res, 403, 'Directory listing disabled', req.method === 'HEAD');
|
|
124
|
+
logRequest(logger, req, res.statusCode, startTime);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await serveDirectoryListing(
|
|
130
|
+
res,
|
|
131
|
+
decodedPath,
|
|
132
|
+
resolvedPath,
|
|
133
|
+
req.method === 'HEAD'
|
|
134
|
+
);
|
|
135
|
+
logRequest(logger, req, res.statusCode, startTime);
|
|
136
|
+
return;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
139
|
+
logger.error('Error generating directory listing', resolvedPath, error);
|
|
140
|
+
logRequest(logger, req, res.statusCode, startTime, error);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
113
143
|
}
|
|
114
144
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
res,
|
|
118
|
-
|
|
119
|
-
resolvedPath,
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
if (stats.isFile()) {
|
|
146
|
+
try {
|
|
147
|
+
await serveFile(res, resolvedPath, stats, req.method === 'HEAD');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.error('Error serving file', resolvedPath, error);
|
|
150
|
+
// Stream errors may happen after headers were sent.
|
|
151
|
+
}
|
|
122
152
|
logRequest(logger, req, res.statusCode, startTime);
|
|
123
153
|
return;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
sendError(res, 500, 'Internal Server Error');
|
|
126
|
-
logger.error('Error generating directory listing', resolvedPath, error);
|
|
127
|
-
logRequest(logger, req, res.statusCode, startTime, error);
|
|
128
|
-
return;
|
|
129
154
|
}
|
|
130
|
-
}
|
|
131
155
|
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
await serveFile(res, resolvedPath, stats, req.method === 'HEAD');
|
|
135
|
-
} catch (error) {
|
|
136
|
-
logger.error('Error serving file', resolvedPath, error);
|
|
137
|
-
// Stream errors may happen after headers were sent.
|
|
138
|
-
}
|
|
156
|
+
sendError(res, 403, 'Forbidden', req.method === 'HEAD');
|
|
139
157
|
logRequest(logger, req, res.statusCode, startTime);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
})().catch(error => {
|
|
159
|
+
if (!res.headersSent) {
|
|
160
|
+
sendError(res, 500, 'Internal Server Error', req.method === 'HEAD');
|
|
161
|
+
} else {
|
|
162
|
+
res.destroy();
|
|
163
|
+
}
|
|
164
|
+
logger.error('Unhandled request error', error);
|
|
165
|
+
logRequest(logger, req, res.statusCode || 500, startTime, error);
|
|
166
|
+
});
|
|
145
167
|
});
|
|
146
168
|
|
|
147
169
|
return server;
|
|
148
170
|
}
|
|
149
171
|
|
|
150
|
-
function sendError(res, statusCode, message) {
|
|
172
|
+
function sendError(res, statusCode, message, headOnly = false) {
|
|
173
|
+
if (res.headersSent) {
|
|
174
|
+
if (!res.writableEnded) {
|
|
175
|
+
res.end();
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
res.statusCode = statusCode;
|
|
152
181
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
153
182
|
res.setHeader('Cache-Control', 'no-store');
|
|
183
|
+
|
|
184
|
+
if (headOnly) {
|
|
185
|
+
res.end();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
res.end(`${statusCode} ${message}`);
|
|
155
190
|
}
|
|
156
191
|
|
|
@@ -173,15 +208,51 @@ async function serveFile(res, filePath, stats, headOnly) {
|
|
|
173
208
|
function streamFile(res, filePath) {
|
|
174
209
|
return new Promise((resolve, reject) => {
|
|
175
210
|
const stream = fs.createReadStream(filePath);
|
|
176
|
-
|
|
211
|
+
let settled = false;
|
|
212
|
+
|
|
213
|
+
const settle = (fn, arg) => {
|
|
214
|
+
if (settled) return;
|
|
215
|
+
settled = true;
|
|
216
|
+
cleanup();
|
|
217
|
+
fn(arg);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const onResponseClose = () => {
|
|
221
|
+
if (!stream.destroyed) {
|
|
222
|
+
stream.destroy();
|
|
223
|
+
}
|
|
224
|
+
settle(resolve);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const onResponseError = error => {
|
|
228
|
+
if (!stream.destroyed) {
|
|
229
|
+
stream.destroy(error);
|
|
230
|
+
}
|
|
231
|
+
settle(reject, error);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const onStreamError = error => {
|
|
177
235
|
if (!res.headersSent) {
|
|
178
236
|
sendError(res, 500, 'Internal Server Error');
|
|
179
237
|
} else {
|
|
180
238
|
res.destroy(error);
|
|
181
239
|
}
|
|
182
|
-
reject
|
|
183
|
-
}
|
|
184
|
-
|
|
240
|
+
settle(reject, error);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const onStreamEnd = () => settle(resolve);
|
|
244
|
+
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
res.removeListener('close', onResponseClose);
|
|
247
|
+
res.removeListener('error', onResponseError);
|
|
248
|
+
stream.removeListener('error', onStreamError);
|
|
249
|
+
stream.removeListener('end', onStreamEnd);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
res.on('close', onResponseClose);
|
|
253
|
+
res.on('error', onResponseError);
|
|
254
|
+
stream.on('error', onStreamError);
|
|
255
|
+
stream.on('end', onStreamEnd);
|
|
185
256
|
stream.pipe(res);
|
|
186
257
|
});
|
|
187
258
|
}
|