@e22m4u/js-http-static-router 0.0.2 → 0.1.0
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 +35 -49
- package/dist/cjs/index.cjs +238 -209
- package/example/server.js +29 -26
- package/example/static/nested/file.txt +11 -0
- package/package.json +4 -4
- package/src/http-static-router.d.ts +27 -22
- package/src/http-static-router.js +224 -222
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/{http-static-route.d.ts → static-route.d.ts} +13 -12
- package/src/static-route.js +91 -0
- package/src/utils/get-pathname-from-url.d.ts +7 -0
- package/src/utils/get-pathname-from-url.js +27 -0
- package/src/utils/get-request-pathname.spec.js +37 -0
- package/src/utils/index.d.ts +1 -1
- package/src/utils/index.js +1 -1
- package/example/static/nested/index.html +0 -10
- package/src/http-static-route.js +0 -73
- package/src/types.d.ts +0 -38
- package/src/utils/normalize-path.d.ts +0 -12
- package/src/utils/normalize-path.js +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e22m4u/js-http-static-router",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "HTTP-маршрутизатор статичных ресурсов для Node.js",
|
|
5
5
|
"author": "Mikhail Evstropov <e22m4u@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"router",
|
|
10
10
|
"http"
|
|
11
11
|
],
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://gitverse.ru/e22m4u/js-http-static-router",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
|
-
"url": "git+https://
|
|
15
|
+
"url": "git+https://gitverse.ru/e22m4u/js-http-static-router.git"
|
|
16
16
|
},
|
|
17
17
|
"type": "module",
|
|
18
18
|
"types": "./src/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"eslint-config-prettier": "~10.1.8",
|
|
55
55
|
"eslint-plugin-chai-expect": "~3.1.0",
|
|
56
56
|
"eslint-plugin-import": "~2.32.0",
|
|
57
|
-
"eslint-plugin-jsdoc": "~62.5.
|
|
57
|
+
"eslint-plugin-jsdoc": "~62.5.4",
|
|
58
58
|
"eslint-plugin-mocha": "~11.2.0",
|
|
59
59
|
"globals": "~17.3.0",
|
|
60
60
|
"husky": "~9.1.7",
|
|
@@ -1,51 +1,56 @@
|
|
|
1
1
|
import {ServerResponse} from 'node:http';
|
|
2
2
|
import {IncomingMessage} from 'node:http';
|
|
3
|
-
import {
|
|
3
|
+
import {StaticRouteDefinition} from './static-route.js';
|
|
4
4
|
import {DebuggableService, ServiceContainer} from '@e22m4u/js-service';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Http static router options.
|
|
8
8
|
*/
|
|
9
9
|
export type HttpStaticRouterOptions = {
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
rootDir?: string;
|
|
11
|
+
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Http static router.
|
|
15
15
|
*/
|
|
16
|
-
export class HttpStaticRouter extends DebuggableService {
|
|
16
|
+
export declare class HttpStaticRouter extends DebuggableService {
|
|
17
17
|
/**
|
|
18
18
|
* Constructor.
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* @param container
|
|
21
|
+
*/
|
|
22
|
+
constructor(container: ServiceContainer);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Constructor.
|
|
26
|
+
*
|
|
20
27
|
* @param options
|
|
21
28
|
*/
|
|
22
|
-
constructor(options
|
|
29
|
+
constructor(options: HttpStaticRouterOptions);
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
|
-
*
|
|
32
|
+
* Constructor.
|
|
26
33
|
*
|
|
27
|
-
* @param
|
|
28
|
-
* @param
|
|
34
|
+
* @param container
|
|
35
|
+
* @param options
|
|
29
36
|
*/
|
|
30
|
-
|
|
37
|
+
constructor(container: ServiceContainer, options: HttpStaticRouterOptions);
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
|
-
*
|
|
40
|
+
* Define route.
|
|
34
41
|
*
|
|
35
|
-
* @param
|
|
42
|
+
* @param routeDef
|
|
36
43
|
*/
|
|
37
|
-
|
|
44
|
+
defineRoute(routeDef: StaticRouteDefinition): this;
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
|
-
*
|
|
47
|
+
* Handle request.
|
|
41
48
|
*
|
|
42
|
-
* @param
|
|
43
|
-
* @param
|
|
44
|
-
* @param res
|
|
49
|
+
* @param request
|
|
50
|
+
* @param response
|
|
45
51
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
): void;
|
|
52
|
+
handleRequest(
|
|
53
|
+
request: IncomingMessage,
|
|
54
|
+
response: ServerResponse,
|
|
55
|
+
): Promise<boolean>;
|
|
51
56
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import mimeTypes from 'mime-types';
|
|
3
|
+
import {IncomingMessage} from 'http';
|
|
3
4
|
import fs, {createReadStream} from 'fs';
|
|
4
|
-
import {
|
|
5
|
+
import {StaticRoute} from './static-route.js';
|
|
6
|
+
import {getPathnameFromUrl} from './utils/index.js';
|
|
5
7
|
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
6
|
-
import {HttpStaticRoute} from './http-static-route.js';
|
|
7
|
-
import {escapeRegexp, normalizePath} from './utils/index.js';
|
|
8
8
|
import {DebuggableService, isServiceContainer} from '@e22m4u/js-service';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} FileInfo
|
|
12
|
+
* @property {string} path
|
|
13
|
+
* @property {number} size
|
|
14
|
+
*/
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Http static router.
|
|
12
18
|
*/
|
|
@@ -15,92 +21,108 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
15
21
|
* Routes.
|
|
16
22
|
*
|
|
17
23
|
* @protected
|
|
18
|
-
* @type {
|
|
24
|
+
* @type {StaticRoute[]}
|
|
19
25
|
*/
|
|
20
26
|
_routes = [];
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Options.
|
|
24
30
|
*
|
|
25
|
-
* @type {
|
|
31
|
+
* @type {import('./http-static-router.js').HttpStaticRouterOptions}
|
|
32
|
+
* @protected
|
|
26
33
|
*/
|
|
27
34
|
_options = {};
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* Constructor.
|
|
31
38
|
*
|
|
32
|
-
* @param {
|
|
39
|
+
* @param {import('@e22m4u/js-service').ServiceContainer|import('./http-static-router.js').HttpStaticRouterOptions} containerOrOptions
|
|
40
|
+
* @param {import('./http-static-router.js').HttpStaticRouterOptions} options
|
|
33
41
|
*/
|
|
34
|
-
constructor(options
|
|
35
|
-
|
|
36
|
-
options = {};
|
|
37
|
-
}
|
|
38
|
-
super(undefined, {
|
|
42
|
+
constructor(containerOrOptions, options) {
|
|
43
|
+
const debugOptions = {
|
|
39
44
|
noEnvironmentNamespace: true,
|
|
40
45
|
namespace: 'jsHttpStaticRouter',
|
|
41
|
-
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
};
|
|
47
|
+
if (isServiceContainer(containerOrOptions)) {
|
|
48
|
+
super(containerOrOptions, debugOptions);
|
|
49
|
+
} else if (containerOrOptions !== undefined) {
|
|
50
|
+
if (
|
|
51
|
+
!containerOrOptions ||
|
|
52
|
+
typeof containerOrOptions !== 'object' ||
|
|
53
|
+
Array.isArray(containerOrOptions)
|
|
54
|
+
) {
|
|
55
|
+
throw new InvalidArgumentError(
|
|
56
|
+
'Parameter "containerOrOptions" must be an Object ' +
|
|
57
|
+
'or an instance of ServiceContainer, but %v was given.',
|
|
58
|
+
options,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
super(undefined, debugOptions);
|
|
62
|
+
if (options === undefined) {
|
|
63
|
+
options = containerOrOptions;
|
|
64
|
+
containerOrOptions = undefined;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
super(undefined, debugOptions);
|
|
47
68
|
}
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
// options
|
|
70
|
+
if (options !== undefined) {
|
|
71
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
50
72
|
throw new InvalidArgumentError(
|
|
51
|
-
'
|
|
52
|
-
options
|
|
73
|
+
'Parameter "options" must be an Object, but %v was given.',
|
|
74
|
+
options,
|
|
53
75
|
);
|
|
54
76
|
}
|
|
77
|
+
// options.rootDir
|
|
78
|
+
if (options.rootDir !== undefined) {
|
|
79
|
+
if (typeof options.rootDir !== 'string') {
|
|
80
|
+
throw new InvalidArgumentError(
|
|
81
|
+
'Option "rootDir" must be a String, but %v was given.',
|
|
82
|
+
options.rootDir,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (!path.isAbsolute(options.rootDir)) {
|
|
86
|
+
throw new InvalidArgumentError(
|
|
87
|
+
'Option "rootDir" must be an absolute path, but %v was given.',
|
|
88
|
+
options.rootDir,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
this._options = {...options};
|
|
55
93
|
}
|
|
56
|
-
this._options
|
|
94
|
+
Object.freeze(this._options);
|
|
57
95
|
}
|
|
58
96
|
|
|
59
97
|
/**
|
|
60
|
-
*
|
|
98
|
+
* Define route.
|
|
61
99
|
*
|
|
62
|
-
* @param {
|
|
63
|
-
* @
|
|
64
|
-
* @returns {object}
|
|
100
|
+
* @param {import('./static-route.js').StaticRouteDefinition} routeDef
|
|
101
|
+
* @returns {this}
|
|
65
102
|
*/
|
|
66
|
-
|
|
67
|
-
if (typeof
|
|
103
|
+
defineRoute(routeDef) {
|
|
104
|
+
if (!routeDef || typeof routeDef !== 'object' || Array.isArray(routeDef)) {
|
|
68
105
|
throw new InvalidArgumentError(
|
|
69
|
-
'
|
|
70
|
-
|
|
106
|
+
'Parameter "routeDef" must be an Object, but %v was given.',
|
|
107
|
+
routeDef,
|
|
71
108
|
);
|
|
72
109
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
if (
|
|
111
|
+
this._options.rootDir !== undefined &&
|
|
112
|
+
!path.isAbsolute(routeDef.resourcePath)
|
|
113
|
+
) {
|
|
114
|
+
routeDef = {...routeDef};
|
|
115
|
+
routeDef.resourcePath = path.join(
|
|
116
|
+
this._options.rootDir,
|
|
117
|
+
routeDef.resourcePath,
|
|
77
118
|
);
|
|
78
119
|
}
|
|
79
|
-
const debug = this.getDebuggerFor(this.
|
|
80
|
-
|
|
120
|
+
const debug = this.getDebuggerFor(this.defineRoute);
|
|
121
|
+
const route = new StaticRoute(routeDef);
|
|
81
122
|
debug('Adding a new route.');
|
|
82
|
-
debug('Resource path is %v.', resourcePath);
|
|
83
|
-
debug('Remote path is %v.', remotePath);
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
stats = fs.statSync(resourcePath);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
// если ресурс не существует в момент старта,
|
|
89
|
-
// это может быть ошибкой конфигурации
|
|
90
|
-
console.error(error);
|
|
91
|
-
throw new InvalidArgumentError(
|
|
92
|
-
'Resource path %v does not exist.',
|
|
93
|
-
resourcePath,
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const isFile = stats.isFile();
|
|
97
|
-
debug('Resource type is %s.', isFile ? 'File' : 'Folder');
|
|
98
|
-
const normalizedRemotePath = normalizePath(remotePath);
|
|
99
|
-
const escapedRemotePath = escapeRegexp(normalizedRemotePath);
|
|
100
|
-
const regexp = isFile
|
|
101
|
-
? new RegExp(`^${escapedRemotePath}/*$`)
|
|
102
|
-
: new RegExp(`^${escapedRemotePath}(?:$|\\/)`);
|
|
103
|
-
const route = new HttpStaticRoute(remotePath, resourcePath, regexp, isFile);
|
|
123
|
+
debug('Resource path is %v.', route.resourcePath);
|
|
124
|
+
debug('Remote path is %v.', route.remotePath);
|
|
125
|
+
debug('Resource type is %s.', route.isFile ? 'File' : 'Folder');
|
|
104
126
|
this._routes.push(route);
|
|
105
127
|
// самые длинные пути проверяются первыми,
|
|
106
128
|
// чтобы избежать коллизий при поиске маршрута
|
|
@@ -109,186 +131,165 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
/**
|
|
112
|
-
*
|
|
134
|
+
* Handle request.
|
|
113
135
|
*
|
|
114
|
-
* @param {IncomingMessage}
|
|
115
|
-
* @
|
|
136
|
+
* @param {import('http').IncomingMessage} request
|
|
137
|
+
* @param {import('http').ServerResponse} response
|
|
138
|
+
* @returns {Promise<boolean>}
|
|
116
139
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
req,
|
|
123
|
-
);
|
|
140
|
+
async handleRequest(request, response) {
|
|
141
|
+
const fileInfo = await this._findFileForRequest(request);
|
|
142
|
+
if (fileInfo !== undefined) {
|
|
143
|
+
this._sendFile(request, response, fileInfo);
|
|
144
|
+
return true;
|
|
124
145
|
}
|
|
125
|
-
|
|
126
|
-
debug('Matching routes with incoming request.');
|
|
127
|
-
const url = (req.url || '/').replace(/\?.*$/, '');
|
|
128
|
-
debug('Incoming request is %s %v.', req.method, url);
|
|
129
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
130
|
-
debug('Method not allowed.');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
debug('Walking through %v routes.', this._routes.length);
|
|
134
|
-
const route = this._routes.find(route => {
|
|
135
|
-
const res = route.regexp.test(url);
|
|
136
|
-
const phrase = res ? 'matched' : 'not matched';
|
|
137
|
-
debug('Resource %v %s.', route.resourcePath, phrase);
|
|
138
|
-
return res;
|
|
139
|
-
});
|
|
140
|
-
route
|
|
141
|
-
? debug('Resource %v matched.', route.resourcePath)
|
|
142
|
-
: debug('No route matched.');
|
|
143
|
-
return route;
|
|
146
|
+
return false;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/**
|
|
147
|
-
*
|
|
150
|
+
* Find file for request.
|
|
148
151
|
*
|
|
149
|
-
* @param {
|
|
150
|
-
* @
|
|
151
|
-
* @param {import('http').ServerResponse} res
|
|
152
|
+
* @param {import('http').IncomingMessage} request
|
|
153
|
+
* @returns {Promise<FileInfo|undefined>|undefined}
|
|
152
154
|
*/
|
|
153
|
-
|
|
154
|
-
if (!(
|
|
155
|
+
async _findFileForRequest(request) {
|
|
156
|
+
if (!(request instanceof IncomingMessage)) {
|
|
155
157
|
throw new InvalidArgumentError(
|
|
156
|
-
'Parameter "
|
|
158
|
+
'Parameter "request" must be an instance of IncomingMessage, ' +
|
|
157
159
|
'but %v was given.',
|
|
158
|
-
|
|
160
|
+
request,
|
|
159
161
|
);
|
|
160
162
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
throw new InvalidArgumentError(
|
|
170
|
-
'Parameter "res" must be an instance of ServerResponse, ' +
|
|
171
|
-
'but %v was given.',
|
|
172
|
-
res,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
const reqUrl = req.url || '/';
|
|
176
|
-
const reqPath = reqUrl.replace(/\?.*$/, '');
|
|
177
|
-
// если параметр "trailingSlash" не активен, и адрес запроса
|
|
178
|
-
// не указывает на корень, но содержит косую черту в конце пути,
|
|
179
|
-
// то косая черта принудительно удаляется и выполняется редирект
|
|
180
|
-
if (
|
|
181
|
-
!this._options.trailingSlash &&
|
|
182
|
-
reqPath !== '/' &&
|
|
183
|
-
/\/$/.test(reqPath)
|
|
184
|
-
) {
|
|
185
|
-
const searchMatch = reqUrl.match(/\?.*$/);
|
|
186
|
-
const search = searchMatch ? searchMatch[0] : '';
|
|
187
|
-
const normalizedPath = reqPath
|
|
188
|
-
.replace(/\/{2,}/g, '/') // удаление дублирующих слешей
|
|
189
|
-
.replace(/\/+$/, ''); // удаление завершающего слеша
|
|
190
|
-
res.writeHead(302, {location: `${normalizedPath}${search}`});
|
|
191
|
-
res.end();
|
|
163
|
+
const debug = this.getDebuggerFor(this._findFileForRequest);
|
|
164
|
+
debug('File finding for an incoming request.');
|
|
165
|
+
debug('Incoming request %s %v.', request.method, request.url);
|
|
166
|
+
let requestPath;
|
|
167
|
+
try {
|
|
168
|
+
requestPath = decodeURIComponent(getPathnameFromUrl(request.url || ''));
|
|
169
|
+
} catch {
|
|
170
|
+
debug('Invalid URL encoding .');
|
|
192
171
|
return;
|
|
193
172
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (/\/{2,}/.test(reqUrl)) {
|
|
197
|
-
const normalizedUrl = reqUrl.replace(/\/{2,}/g, '/');
|
|
198
|
-
res.writeHead(302, {location: normalizedUrl});
|
|
199
|
-
res.end();
|
|
173
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
174
|
+
debug('Method not allowed.');
|
|
200
175
|
return;
|
|
201
176
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
let targetPath = route.resourcePath;
|
|
206
|
-
if (!route.isFile) {
|
|
207
|
-
// извлечение относительного пути в дополнение к адресу
|
|
208
|
-
// ресурса путем удаления из адреса запроса той части,
|
|
209
|
-
// которая была указана при объявлении маршрута
|
|
210
|
-
const relativePath = reqPath.replace(route.regexp, '');
|
|
211
|
-
// объединение адреса ресурса
|
|
212
|
-
// с дополнительной частью
|
|
213
|
-
targetPath = path.join(route.resourcePath, relativePath);
|
|
177
|
+
if (requestPath.includes('//')) {
|
|
178
|
+
debug('Request path contains duplicate slashes.');
|
|
179
|
+
return;
|
|
214
180
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
targetPath = path.resolve(targetPath);
|
|
218
|
-
const resourceRoot = path.resolve(route.resourcePath);
|
|
219
|
-
if (!targetPath.startsWith(resourceRoot)) {
|
|
220
|
-
res.writeHead(403, {'content-type': 'text/plain'});
|
|
221
|
-
res.end('403 Forbidden');
|
|
181
|
+
if (!this._routes.length) {
|
|
182
|
+
debug('No registered routes.');
|
|
222
183
|
return;
|
|
223
184
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const search = searchMatch ? searchMatch[0] : '';
|
|
242
|
-
const normalizedPath = reqPath.replace(/\/{2,}/g, '/');
|
|
243
|
-
res.writeHead(302, {location: `${normalizedPath}/${search}`});
|
|
244
|
-
res.end();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
185
|
+
debug('Walking through %v routes.', this._routes.length);
|
|
186
|
+
for (const route of this._routes) {
|
|
187
|
+
const isMatched = route.regexp.test(requestPath);
|
|
188
|
+
if (isMatched) {
|
|
189
|
+
debug('Matched route %v.', route.remotePath);
|
|
190
|
+
// если ресурс ссылается на папку, то из адреса запроса
|
|
191
|
+
// извлекается дополнительная часть (если присутствует),
|
|
192
|
+
// и формируется целевой путь файловой системы
|
|
193
|
+
let targetPath = route.resourcePath;
|
|
194
|
+
if (!route.isFile) {
|
|
195
|
+
// извлечение относительного пути в дополнение к адресу
|
|
196
|
+
// ресурса путем удаления из адреса запроса той части,
|
|
197
|
+
// которая была указана при объявлении маршрута
|
|
198
|
+
const relativePath = requestPath.replace(route.regexp, '');
|
|
199
|
+
// объединение адреса ресурса
|
|
200
|
+
// с дополнительной частью
|
|
201
|
+
targetPath = path.join(route.resourcePath, relativePath);
|
|
247
202
|
}
|
|
248
|
-
// если
|
|
249
|
-
// то
|
|
250
|
-
targetPath = path.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const searchMatch = reqUrl.match(/\?.*$/);
|
|
257
|
-
const search = searchMatch ? searchMatch[0] : '';
|
|
258
|
-
const normalizedPath = reqPath
|
|
259
|
-
.replace(/\/{2,}/g, '/') // удаление дублирующих слешей
|
|
260
|
-
.replace(/\/+$/, ''); // удаление завершающего слеша
|
|
261
|
-
res.writeHead(302, {location: `${normalizedPath}${search}`});
|
|
262
|
-
res.end();
|
|
203
|
+
// если обнаружена попытка выхода за пределы
|
|
204
|
+
// директории маршрута, то возвращается undefined
|
|
205
|
+
targetPath = path.resolve(targetPath);
|
|
206
|
+
const resourceRoot = path.resolve(route.resourcePath);
|
|
207
|
+
if (
|
|
208
|
+
targetPath !== resourceRoot &&
|
|
209
|
+
!targetPath.startsWith(resourceRoot + path.sep)
|
|
210
|
+
) {
|
|
263
211
|
return;
|
|
264
212
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
213
|
+
// с определением размера файла одновременно выполняется
|
|
214
|
+
// отсечение отсутствующих файлов и директорий, в таких
|
|
215
|
+
// случаях значение размера будет является undefined
|
|
216
|
+
const fileSize = await new Promise(resolve => {
|
|
217
|
+
fs.stat(targetPath, (statsError, stats) => {
|
|
218
|
+
if (statsError || stats.isDirectory()) {
|
|
219
|
+
resolve(undefined);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
resolve(stats.size);
|
|
223
|
+
return;
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
// если размер файла определен, то поиск
|
|
227
|
+
// прерывается и возвращается информация
|
|
228
|
+
if (fileSize !== undefined) {
|
|
229
|
+
// если файл найден, но запрос заканчивается
|
|
230
|
+
// на слеш, то файл должен быть проигнорирован
|
|
231
|
+
if (requestPath.endsWith('/')) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
debug('File found %v.', targetPath);
|
|
235
|
+
return {path: targetPath, size: fileSize};
|
|
286
236
|
}
|
|
287
|
-
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
debug('File not found.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Send file.
|
|
244
|
+
*
|
|
245
|
+
* @param {import('http').IncomingMessage} request
|
|
246
|
+
* @param {import('http').ServerResponse} response
|
|
247
|
+
* @param {FileInfo} fileInfo
|
|
248
|
+
*/
|
|
249
|
+
_sendFile(request, response, fileInfo) {
|
|
250
|
+
const debug = this.getDebuggerFor(this._sendFile);
|
|
251
|
+
debug('File sending for an incoming request.');
|
|
252
|
+
debug('Incoming request %s %v.', request.method, request.url);
|
|
253
|
+
debug('File path %v.', fileInfo.path);
|
|
254
|
+
debug('File size %v bytes.', fileInfo.size);
|
|
255
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
256
|
+
debug('Method not allowed.');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// формирование заголовка "content-type"
|
|
260
|
+
// в зависимости от расширения файла
|
|
261
|
+
const extname = path.extname(fileInfo.path);
|
|
262
|
+
const contentType =
|
|
263
|
+
mimeTypes.contentType(extname) || 'application/octet-stream';
|
|
264
|
+
// файл читается и отправляется частями,
|
|
265
|
+
// что значительно снижает использование памяти
|
|
266
|
+
const fileStream = createReadStream(fileInfo.path);
|
|
267
|
+
fileStream.on('error', error => {
|
|
268
|
+
debug('Unable to open a file stream.');
|
|
269
|
+
this._handleFsError(error, response);
|
|
270
|
+
});
|
|
271
|
+
// отправка заголовка 200, только после
|
|
272
|
+
// этого начинается отдача файла
|
|
273
|
+
fileStream.on('open', () => {
|
|
274
|
+
response.writeHead(200, {
|
|
275
|
+
'Content-Type': contentType,
|
|
276
|
+
'Content-Length': fileInfo.size,
|
|
288
277
|
});
|
|
289
|
-
|
|
278
|
+
// для HEAD запроса отправляются
|
|
279
|
+
// только заголовки (без тела)
|
|
280
|
+
if (request.method === 'HEAD') {
|
|
281
|
+
response.end();
|
|
282
|
+
debug('Response has been sent without a body for the HEAD request.');
|
|
283
|
+
// важно закрыть файловый поток, чтобы операционная
|
|
284
|
+
// система не исчерпала лимит открытых файлов
|
|
290
285
|
fileStream.destroy();
|
|
291
|
-
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
fileStream.pipe(response);
|
|
289
|
+
});
|
|
290
|
+
request.on('close', () => {
|
|
291
|
+
debug('File has been sent.');
|
|
292
|
+
fileStream.destroy();
|
|
292
293
|
});
|
|
293
294
|
}
|
|
294
295
|
|
|
@@ -296,21 +297,22 @@ export class HttpStaticRouter extends DebuggableService {
|
|
|
296
297
|
* Handle filesystem error.
|
|
297
298
|
*
|
|
298
299
|
* @param {object} error
|
|
299
|
-
* @param {object}
|
|
300
|
+
* @param {object} response
|
|
300
301
|
* @returns {undefined}
|
|
301
302
|
*/
|
|
302
|
-
_handleFsError(error,
|
|
303
|
-
if (
|
|
303
|
+
_handleFsError(error, response) {
|
|
304
|
+
if (response.headersSent) {
|
|
305
|
+
response.destroy();
|
|
304
306
|
return;
|
|
305
307
|
}
|
|
306
308
|
if ('code' in error && error.code === 'ENOENT') {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
response.writeHead(404, {'Content-Type': 'text/plain'});
|
|
310
|
+
response.write('404 Not Found');
|
|
311
|
+
response.end();
|
|
310
312
|
} else {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
response.writeHead(500, {'Content-Type': 'text/plain'});
|
|
314
|
+
response.write('500 Internal Server Error');
|
|
315
|
+
response.end();
|
|
314
316
|
}
|
|
315
317
|
}
|
|
316
318
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './static-route.js';
|
|
2
2
|
export * from './http-static-router.js';
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './static-route.js';
|
|
2
2
|
export * from './http-static-router.js';
|