@chenpu17/serve-here 1.0.1 → 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 +8 -1
- 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
|
@@ -260,8 +260,16 @@ function main() {
|
|
|
260
260
|
const port = options.port || DEFAULT_PORT;
|
|
261
261
|
const host = options.host || DEFAULT_HOST;
|
|
262
262
|
|
|
263
|
+
if (options.daemonChild) {
|
|
264
|
+
ensureDirectories();
|
|
265
|
+
process.on('exit', () => removePidFile(port));
|
|
266
|
+
}
|
|
267
|
+
|
|
263
268
|
server.on('error', error => {
|
|
264
269
|
console.error('Failed to start server:', error.message);
|
|
270
|
+
if (options.daemonChild) {
|
|
271
|
+
removePidFile(port);
|
|
272
|
+
}
|
|
265
273
|
process.exit(1);
|
|
266
274
|
});
|
|
267
275
|
|
|
@@ -275,7 +283,6 @@ function main() {
|
|
|
275
283
|
|
|
276
284
|
// Write PID file when running as daemon child
|
|
277
285
|
if (options.daemonChild) {
|
|
278
|
-
ensureDirectories();
|
|
279
286
|
writePidFile(port, process.pid, rootDir);
|
|
280
287
|
}
|
|
281
288
|
});
|
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
|
}
|