@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenpu17/serve-here",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A minimal CLI to serve static files from a directory over HTTP.",
5
5
  "bin": {
6
6
  "serve-here": "bin/serve-here.js"
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(async (req, res) => {
33
+ const server = http.createServer((req, res) => {
34
34
  const startTime = Date.now();
35
35
 
36
- if (!req.url) {
37
- sendError(res, 400, 'Bad Request');
38
- return;
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
- let decodedPath;
49
- try {
50
- const url = new URL(req.url, 'http://localhost');
51
- decodedPath = decodeURIComponent(url.pathname);
52
- } catch (error) {
53
- sendError(res, 400, 'Bad Request');
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
- const candidateSegments = decodedPath
59
- .split('/')
60
- .filter(segment => segment && segment !== '.');
61
- const resolvedPath = path.resolve(resolvedRoot, ...candidateSegments);
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
- if (!resolvedPath.startsWith(rootWithSep) && resolvedPath !== resolvedRoot) {
64
- sendError(res, 403, 'Forbidden');
65
- logRequest(logger, req, res.statusCode, startTime);
66
- return;
67
- }
63
+ const candidateSegments = decodedPath
64
+ .split('/')
65
+ .filter(segment => segment && segment !== '.');
66
+ const resolvedPath = path.resolve(resolvedRoot, ...candidateSegments);
68
67
 
69
- let stats;
70
- try {
71
- stats = await stat(resolvedPath);
72
- } catch (error) {
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
- if (stats.isDirectory()) {
84
- if (!decodedPath.endsWith('/')) {
85
- // Align with browser expectations for relative asset loading.
86
- res.statusCode = 301;
87
- res.setHeader('Location', `${decodedPath}/`);
88
- res.end();
89
- logRequest(logger, req, res.statusCode, startTime);
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
- for (const indexFile of indexFiles) {
94
- const candidate = path.join(resolvedPath, indexFile);
95
- try {
96
- const indexStats = await stat(candidate);
97
- if (indexStats.isFile()) {
98
- await serveFile(res, candidate, indexStats, req.method === 'HEAD');
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
- } catch (error) {
103
- if (error.code !== 'ENOENT') {
104
- logger.error('Error reading index file', candidate, error);
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
- if (!enableDirectoryListing) {
110
- sendError(res, 403, 'Directory listing disabled');
111
- logRequest(logger, req, res.statusCode, startTime);
112
- return;
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
- try {
116
- await serveDirectoryListing(
117
- res,
118
- decodedPath,
119
- resolvedPath,
120
- req.method === 'HEAD'
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
- if (stats.isFile()) {
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
- return;
141
- }
142
-
143
- sendError(res, 403, 'Forbidden');
144
- logRequest(logger, req, res.statusCode, startTime);
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
- stream.on('error', error => {
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(error);
183
- });
184
- stream.on('end', resolve);
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
  }