@e22m4u/js-http-static-router 0.1.2 → 0.2.1
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/.mocharc.json +2 -1
- package/README.md +265 -17
- package/build-cjs.js +1 -1
- package/dist/cjs/index.cjs +114 -76
- package/example/server.js +23 -13
- package/mocha.setup.js +4 -0
- package/package.json +10 -8
- package/src/http-static-router.d.ts +7 -2
- package/src/http-static-router.js +73 -61
- package/src/http-static-router.spec.js +899 -0
- package/src/static-route.js +18 -8
- package/src/types.d.ts +7 -0
- package/src/utils/create-cookie-string.d.ts +6 -0
- package/src/utils/create-cookie-string.js +28 -0
- package/src/utils/create-cookie-string.spec.js +32 -0
- package/src/utils/create-error.d.ts +14 -0
- package/src/utils/create-error.js +29 -0
- package/src/utils/create-error.spec.js +42 -0
- package/src/utils/create-request-mock.d.ts +35 -0
- package/src/utils/create-request-mock.js +483 -0
- package/src/utils/create-request-mock.spec.js +646 -0
- package/src/utils/create-response-mock.d.ts +18 -0
- package/src/utils/create-response-mock.js +131 -0
- package/src/utils/create-response-mock.spec.js +150 -0
- package/src/utils/fetch-request-body.d.ts +26 -0
- package/src/utils/fetch-request-body.js +148 -0
- package/src/utils/fetch-request-body.spec.js +209 -0
- package/src/utils/get-pathname-from-url.d.ts +1 -1
- package/src/utils/{get-request-pathname.spec.js → get-pathname-from-url.spec.js} +1 -1
- package/src/utils/index.d.ts +8 -0
- package/src/utils/index.js +8 -0
- package/src/utils/is-readable-stream.d.ts +9 -0
- package/src/utils/is-readable-stream.js +12 -0
- package/src/utils/is-readable-stream.spec.js +23 -0
- package/src/utils/parse-content-type.d.ts +15 -0
- package/src/utils/parse-content-type.js +34 -0
- package/src/utils/parse-content-type.spec.js +79 -0
- package/src/utils/parse-cookie-string.d.ts +19 -0
- package/src/utils/parse-cookie-string.js +36 -0
- package/src/utils/parse-cookie-string.spec.js +45 -0
- package/{example/static → static}/index.html +2 -2
- /package/{example/static/nested/file.txt → static/assets/nested/heart.txt} +0 -0
- /package/{example/static/file.txt → static/assets/rabbit.txt} +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import mimeTypes from 'mime-types';
|
|
3
|
-
import {IncomingMessage} from 'http';
|
|
4
3
|
import fs, {createReadStream} from 'fs';
|
|
5
4
|
import {StaticRoute} from './static-route.js';
|
|
6
5
|
import {getPathnameFromUrl} from './utils/index.js';
|
|
@@ -41,7 +40,7 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
41
40
|
*/
|
|
42
41
|
constructor(containerOrOptions, options) {
|
|
43
42
|
const debugOptions = {
|
|
44
|
-
|
|
43
|
+
noGlobalNamespace: true,
|
|
45
44
|
namespace: 'jsHttpStaticRouter',
|
|
46
45
|
};
|
|
47
46
|
if (isServiceContainer(containerOrOptions)) {
|
|
@@ -53,18 +52,18 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
53
52
|
Array.isArray(containerOrOptions)
|
|
54
53
|
) {
|
|
55
54
|
throw new InvalidArgumentError(
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
|
|
55
|
+
'First parameter must be an Object or an instance ' +
|
|
56
|
+
'of ServiceContainer, but %v was given.',
|
|
57
|
+
containerOrOptions,
|
|
59
58
|
);
|
|
60
59
|
}
|
|
61
|
-
super(
|
|
60
|
+
super(debugOptions);
|
|
62
61
|
if (options === undefined) {
|
|
63
62
|
options = containerOrOptions;
|
|
64
63
|
containerOrOptions = undefined;
|
|
65
64
|
}
|
|
66
65
|
} else {
|
|
67
|
-
super(
|
|
66
|
+
super(debugOptions);
|
|
68
67
|
}
|
|
69
68
|
// options
|
|
70
69
|
if (options !== undefined) {
|
|
@@ -98,15 +97,23 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
98
97
|
* Define route.
|
|
99
98
|
*
|
|
100
99
|
* @param {import('./static-route.js').StaticRouteDefinition} routeDef
|
|
101
|
-
* @returns {
|
|
100
|
+
* @returns {import('./static-route.js').StaticRoute}
|
|
102
101
|
*/
|
|
103
102
|
defineRoute(routeDef) {
|
|
103
|
+
// options
|
|
104
104
|
if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef)) {
|
|
105
105
|
throw new InvalidArgumentError(
|
|
106
106
|
'Parameter "routeDef" must be an Object, but %v was given.',
|
|
107
107
|
routeDef,
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
|
+
// options.resourcePath
|
|
111
|
+
if (typeof routeDef.resourcePath !== 'string') {
|
|
112
|
+
throw new InvalidArgumentError(
|
|
113
|
+
'Option "resourcePath" must be a String, but %v was given.',
|
|
114
|
+
routeDef.resourcePath,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
110
117
|
if (
|
|
111
118
|
this._options.baseDir !== undefined &&
|
|
112
119
|
!path.isAbsolute(routeDef.resourcePath)
|
|
@@ -117,22 +124,37 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
117
124
|
routeDef.resourcePath,
|
|
118
125
|
);
|
|
119
126
|
}
|
|
127
|
+
if (
|
|
128
|
+
this._options.baseDir === undefined &&
|
|
129
|
+
!path.isAbsolute(routeDef.resourcePath)
|
|
130
|
+
) {
|
|
131
|
+
throw new InvalidArgumentError(
|
|
132
|
+
'Option "resourcePath" must be an absolute path when the router ' +
|
|
133
|
+
'option "basePath" is not specified, but %v was given.',
|
|
134
|
+
routeDef.resourcePath,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
120
137
|
const debug = this.getDebuggerFor(this.defineRoute);
|
|
121
138
|
const route = new StaticRoute(routeDef);
|
|
122
139
|
debug('Adding a new route.');
|
|
123
|
-
debug('Resource path is %v.', route.resourcePath);
|
|
124
140
|
debug('Remote path is %v.', route.remotePath);
|
|
141
|
+
debug('Resource path is %v.', route.resourcePath);
|
|
125
142
|
debug('Resource type is %s.', route.isFile ? 'File' : 'Folder');
|
|
126
143
|
this._routes.push(route);
|
|
127
144
|
// самые длинные пути проверяются первыми,
|
|
128
145
|
// чтобы избежать коллизий при поиске маршрута
|
|
129
146
|
this._routes.sort((a, b) => b.remotePath.length - a.remotePath.length);
|
|
130
|
-
return
|
|
147
|
+
return route;
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
/**
|
|
134
151
|
* Handle request.
|
|
135
152
|
*
|
|
153
|
+
* Метод возвращает Promise, который разрешается как:
|
|
154
|
+
* - false, если маршрут не совпал, файл не найден
|
|
155
|
+
* или метод запроса не поддерживается;
|
|
156
|
+
* - true, во всех остальных случаях;
|
|
157
|
+
*
|
|
136
158
|
* @param {import('http').IncomingMessage} request
|
|
137
159
|
* @param {import('http').ServerResponse} response
|
|
138
160
|
* @returns {Promise<boolean>}
|
|
@@ -140,7 +162,7 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
140
162
|
async handleRequest(request, response) {
|
|
141
163
|
const fileInfo = await this._findFileForRequest(request);
|
|
142
164
|
if (fileInfo !== undefined) {
|
|
143
|
-
this._sendFile(request, response, fileInfo);
|
|
165
|
+
await this._sendFile(request, response, fileInfo);
|
|
144
166
|
return true;
|
|
145
167
|
}
|
|
146
168
|
return false;
|
|
@@ -153,13 +175,6 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
153
175
|
* @returns {Promise<FileInfo|undefined>|undefined}
|
|
154
176
|
*/
|
|
155
177
|
async _findFileForRequest(request) {
|
|
156
|
-
if (!(request instanceof IncomingMessage)) {
|
|
157
|
-
throw new InvalidArgumentError(
|
|
158
|
-
'Parameter "request" must be an instance of IncomingMessage, ' +
|
|
159
|
-
'but %v was given.',
|
|
160
|
-
request,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
178
|
const debug = this.getDebuggerFor(this._findFileForRequest);
|
|
164
179
|
debug('File finding for an incoming request.');
|
|
165
180
|
debug('Incoming request %s %v.', request.method, request.url);
|
|
@@ -184,21 +199,22 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
184
199
|
}
|
|
185
200
|
debug('Walking through %v routes.', this._routes.length);
|
|
186
201
|
for (const route of this._routes) {
|
|
187
|
-
const isMatched = route.regexp.test(requestPath);
|
|
202
|
+
const isMatched = route.regexp.test(requestPath || '/');
|
|
188
203
|
if (isMatched) {
|
|
189
204
|
debug('Matched route %v.', route.remotePath);
|
|
190
205
|
// если ресурс ссылается на папку, то из адреса запроса
|
|
191
206
|
// извлекается дополнительная часть (если присутствует),
|
|
192
207
|
// и формируется целевой путь файловой системы
|
|
193
208
|
let targetPath = route.resourcePath;
|
|
209
|
+
let extraPath = ''; // дополнительная часть адреса
|
|
194
210
|
if (!route.isFile) {
|
|
195
211
|
// извлечение относительного пути в дополнение к адресу
|
|
196
212
|
// ресурса путем удаления из адреса запроса той части,
|
|
197
213
|
// которая была указана при объявлении маршрута
|
|
198
|
-
|
|
214
|
+
extraPath = requestPath.replace(route.regexp, '');
|
|
199
215
|
// объединение адреса ресурса
|
|
200
216
|
// с дополнительной частью
|
|
201
|
-
targetPath = path.join(route.resourcePath,
|
|
217
|
+
targetPath = path.join(route.resourcePath, extraPath);
|
|
202
218
|
}
|
|
203
219
|
// если обнаружена попытка выхода за пределы
|
|
204
220
|
// директории маршрута, то возвращается undefined
|
|
@@ -226,17 +242,17 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
226
242
|
// если размер файла определен, то поиск
|
|
227
243
|
// прерывается и возвращается информация
|
|
228
244
|
if (fileSize !== undefined) {
|
|
229
|
-
// если файл найден,
|
|
230
|
-
// на слеш, то файл
|
|
231
|
-
if (
|
|
245
|
+
// если файл найден, и дополнительная часть запроса
|
|
246
|
+
// заканчивается на слеш, то файл игнорируется
|
|
247
|
+
if (extraPath && extraPath.endsWith('/')) {
|
|
232
248
|
continue;
|
|
233
249
|
}
|
|
234
|
-
debug('
|
|
250
|
+
debug('Found file %v.', targetPath);
|
|
235
251
|
return {path: targetPath, size: fileSize};
|
|
236
252
|
}
|
|
237
253
|
}
|
|
238
254
|
}
|
|
239
|
-
debug('File not found.');
|
|
255
|
+
debug('File was not found.');
|
|
240
256
|
}
|
|
241
257
|
|
|
242
258
|
/**
|
|
@@ -245,17 +261,16 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
245
261
|
* @param {import('http').IncomingMessage} request
|
|
246
262
|
* @param {import('http').ServerResponse} response
|
|
247
263
|
* @param {FileInfo} fileInfo
|
|
264
|
+
* @returns {Promise<void>}
|
|
248
265
|
*/
|
|
249
|
-
_sendFile(request, response, fileInfo) {
|
|
266
|
+
async _sendFile(request, response, fileInfo) {
|
|
267
|
+
let resolve;
|
|
268
|
+
const promise = new Promise(res => (resolve = res));
|
|
250
269
|
const debug = this.getDebuggerFor(this._sendFile);
|
|
251
270
|
debug('File sending for an incoming request.');
|
|
252
271
|
debug('Incoming request %s %v.', request.method, request.url);
|
|
253
272
|
debug('File path %v.', fileInfo.path);
|
|
254
273
|
debug('File size %v bytes.', fileInfo.size);
|
|
255
|
-
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
256
|
-
debug('Method not allowed.');
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
274
|
// формирование заголовка "content-type"
|
|
260
275
|
// в зависимости от расширения файла
|
|
261
276
|
const extname = path.extname(fileInfo.path);
|
|
@@ -266,15 +281,24 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
266
281
|
const fileStream = createReadStream(fileInfo.path);
|
|
267
282
|
fileStream.on('error', error => {
|
|
268
283
|
debug('Unable to open a file stream.');
|
|
269
|
-
|
|
284
|
+
console.error(error);
|
|
285
|
+
if (response.headersSent) {
|
|
286
|
+
response.destroy();
|
|
287
|
+
resolve();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
response.statusCode = 500;
|
|
291
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
292
|
+
response.write('500 Internal Server Error');
|
|
293
|
+
response.end();
|
|
294
|
+
resolve();
|
|
270
295
|
});
|
|
271
296
|
// отправка заголовка 200, только после
|
|
272
297
|
// этого начинается отдача файла
|
|
273
298
|
fileStream.on('open', () => {
|
|
274
|
-
response.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
299
|
+
response.statusCode = 200;
|
|
300
|
+
response.setHeader('Content-Type', contentType);
|
|
301
|
+
response.setHeader('Content-Length', fileInfo.size);
|
|
278
302
|
// для HEAD запроса отправляются
|
|
279
303
|
// только заголовки (без тела)
|
|
280
304
|
if (request.method === 'HEAD') {
|
|
@@ -288,31 +312,19 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
288
312
|
fileStream.pipe(response);
|
|
289
313
|
});
|
|
290
314
|
request.on('close', () => {
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
// если ответ еще не закончен,
|
|
316
|
+
// значит клиент оборвал соединение
|
|
317
|
+
if (!response.writableFinished) {
|
|
318
|
+
debug('Request closed prematurely by the client.');
|
|
319
|
+
fileStream.destroy();
|
|
320
|
+
resolve();
|
|
321
|
+
}
|
|
293
322
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
* @param {object} response
|
|
301
|
-
* @returns {undefined}
|
|
302
|
-
*/
|
|
303
|
-
_handleFsError(error, response) {
|
|
304
|
-
if (response.headersSent) {
|
|
305
|
-
response.destroy();
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if ('code' in error && error.code === 'ENOENT') {
|
|
309
|
-
response.writeHead(404, {'Content-Type': 'text/plain'});
|
|
310
|
-
response.write('404 Not Found');
|
|
311
|
-
response.end();
|
|
312
|
-
} else {
|
|
313
|
-
response.writeHead(500, {'Content-Type': 'text/plain'});
|
|
314
|
-
response.write('500 Internal Server Error');
|
|
315
|
-
response.end();
|
|
316
|
-
}
|
|
323
|
+
// успешное завершение отправки ответа
|
|
324
|
+
response.on('finish', () => {
|
|
325
|
+
debug('File has been sent successfully.');
|
|
326
|
+
resolve();
|
|
327
|
+
});
|
|
328
|
+
return promise;
|
|
317
329
|
}
|
|
318
330
|
}
|