lupine.api 1.1.47 → 1.1.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.47",
3
+ "version": "1.1.49",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -10,6 +10,10 @@ export const handler304 = (res: ServerResponse, msg?: string | object, contentTy
10
10
  handlerResponse(res, 304, '304 Not Modified', msg, contentType);
11
11
  };
12
12
 
13
+ export const handler400 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
14
+ handlerResponse(res, 400, '400 Bad Request', msg, contentType);
15
+ };
16
+
13
17
  export const handler403 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
14
18
  handlerResponse(res, 403, '403 Forbidden', msg, contentType);
15
19
  };
@@ -30,8 +34,8 @@ export const handler500 = (res: ServerResponse, msg?: string | object, contentTy
30
34
  handlerResponse(res, 500, '500 Internal Server Error', msg, contentType);
31
35
  };
32
36
 
33
- export const handler400 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
34
- handlerResponse(res, 400, '400 Bad Request', msg, contentType);
37
+ export const handler503 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
38
+ handlerResponse(res, 503, '503 Service Unavailable', msg, contentType);
35
39
  };
36
40
 
37
41
  export const handlerResponse = (
@@ -0,0 +1,274 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import { Logger } from '../lib/logger';
3
+ import crypto from 'crypto';
4
+ import { parseCookies } from '../lib/utils/cookie-util';
5
+ import { WebProcessor } from './web-processor';
6
+ import { handler403, handler404, handler500, handler503, SimpleStorage } from '../api';
7
+ import { JsonObject, AsyncStorageProps, ServerRequest, SetCookieProps } from '../models';
8
+ import { HostToPath } from './host-to-path';
9
+ import { serializeCookie } from '../lib/utils/cookie-util';
10
+ const logger = new Logger('listener');
11
+
12
+ let MAX_REQUEST_SIZE = 1024 * 1024 * 5;
13
+ export const setMaxRequestSize = (size: number) => {
14
+ MAX_REQUEST_SIZE = size;
15
+ };
16
+
17
+ // The maximum number of requests being processed. If there are no requests for 10 minutes, this number will be reset to 0.
18
+ let MAX_REQUEST_COUNT = 100;
19
+ let REQUEST_COUNT = 0;
20
+ export const setMaxRequestCount = (count: number) => {
21
+ MAX_REQUEST_COUNT = count;
22
+ };
23
+ export const getRequestCount = () => {
24
+ return REQUEST_COUNT;
25
+ };
26
+
27
+ let REQUEST_TIMEOUT = 1000 * 30;
28
+ export const setRequestTimeout = (timeout: number) => {
29
+ REQUEST_TIMEOUT = timeout;
30
+ };
31
+
32
+ let accessControlAllowHosts: string[] = [];
33
+ export const setAccessControlAllowHost = (allowHosts: string[]) => {
34
+ accessControlAllowHosts = allowHosts;
35
+ };
36
+
37
+ let SERVER_NAME: string = 'nginx/1.19.2';
38
+ export const setServerName = (serverName: string) => {
39
+ SERVER_NAME = serverName;
40
+ };
41
+
42
+ let IP_LIMIT_RATE_PER_SECOND = 3;
43
+ export const setIpLimitRatePerSecond = (rate: number) => {
44
+ IP_LIMIT_RATE_PER_SECOND = rate;
45
+ };
46
+ let IP_LIMIT_RATE_PER_MINUTE = 60;
47
+ export const setIpLimitRatePerMinute = (rate: number) => {
48
+ IP_LIMIT_RATE_PER_MINUTE = rate;
49
+ };
50
+
51
+ let IP_BLOCK_TIME = 60_000;
52
+ export const setIpBlockTime = (time: number) => {
53
+ IP_BLOCK_TIME = time;
54
+ };
55
+
56
+ export const rawMiddlewareIpLimit = (req: IncomingMessage, res: ServerResponse, next: () => void) => {
57
+ const clientIp = req.socket.remoteAddress || 'unknown';
58
+ };
59
+
60
+ export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
61
+
62
+ let lastRequestTime = new Date().getTime();
63
+
64
+ // type ProcessRequest = (req: ServerRequest, res: ServerResponse) => void;
65
+ export class WebListener {
66
+ // process requests before business logic, for example IP filter, rate limit, etc.
67
+ rawMiddlewares: RawMiddleware[];
68
+ processor: WebProcessor;
69
+
70
+ constructor(processRequest: WebProcessor) {
71
+ this.rawMiddlewares = [];
72
+ this.processor = processRequest;
73
+ }
74
+
75
+ addRawMiddlewareChain(middleware: RawMiddleware) {
76
+ this.rawMiddlewares.push(middleware);
77
+ }
78
+ async handleRawMiddlewares(req: IncomingMessage, res: ServerResponse) {
79
+ const runChain = (list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) => {
80
+ const dispatch = async (i: number) => {
81
+ const fn = list[i];
82
+ if (!fn) return;
83
+ await fn(context.req, context.res, () => dispatch(i + 1));
84
+ };
85
+ return dispatch(0);
86
+ };
87
+ await runChain(this.rawMiddlewares, { req, res });
88
+ }
89
+
90
+ async listener(reqOrigin: IncomingMessage, res: ServerResponse) {
91
+ // If there is no request in the last 10 minutes, reset the request count.
92
+ if (new Date().getTime() - lastRequestTime > 1000 * 60 * 10) {
93
+ if (REQUEST_COUNT != 0) {
94
+ // in case any errors skipped (--REQUEST_COUNT)
95
+ logger.warn(`!!!!!!!!!! ========== REQUEST_COUNT is not counted properly: ${REQUEST_COUNT}`);
96
+ }
97
+ REQUEST_COUNT = 0;
98
+ lastRequestTime = new Date().getTime();
99
+ }
100
+
101
+ // back-pressure
102
+ if (REQUEST_COUNT > MAX_REQUEST_COUNT) {
103
+ logger.warn(`Too many requests, count: ${REQUEST_COUNT} > ${MAX_REQUEST_COUNT}`);
104
+ handler503(res, 'Server is busy, please retry later.');
105
+ return;
106
+ }
107
+
108
+ await this.handleRawMiddlewares(reqOrigin, res);
109
+ if (res.writableEnded || res.headersSent) {
110
+ return;
111
+ }
112
+
113
+ // const requestStart = process.hrtime.bigint();
114
+ const uuid = crypto.randomUUID();
115
+ const url = reqOrigin.url || '';
116
+ const requestInfo = `uuid: ${uuid}, Access url: ${url}`;
117
+ const req = reqOrigin as ServerRequest;
118
+
119
+ const host = (req.headers.host || '').split(':')[0]; // req.headers.host contains port
120
+ const hostPath = HostToPath.findHostPath(host);
121
+ if (!hostPath || !hostPath.webPath || !hostPath.appName) {
122
+ const msg = `Web root is not defined properly for host: ${host}.`;
123
+ logger.error(msg);
124
+ handler404(res, msg);
125
+ return;
126
+ }
127
+
128
+ REQUEST_COUNT++;
129
+ logger.debug(
130
+ `Request started. Count: ${REQUEST_COUNT}, Log uuid: ${uuid}, access: ${
131
+ req.headers.host
132
+ }, url: ${url}, time: ${new Date().toISOString()}, from: ${req.socket.remoteAddress}`
133
+ );
134
+
135
+ const urlSplit = url.split('?');
136
+ req.setTimeout(REQUEST_TIMEOUT);
137
+ req.on('timeout', () => {
138
+ REQUEST_COUNT--;
139
+ logger.warn('timeout');
140
+ req.destroy(new Error('timeout handling'));
141
+ });
142
+
143
+ const jsonFn = (): JsonObject | undefined => {
144
+ if (!req.locals._json && req.locals.body) {
145
+ const sBody = req.locals.body.toString();
146
+ if (!sBody) {
147
+ req.locals._json = undefined;
148
+ } else {
149
+ try {
150
+ req.locals._json = JSON.parse(sBody);
151
+ } catch (err: any) {
152
+ logger.warn(`JSON.parse error: ${err.message}`);
153
+ }
154
+ }
155
+ }
156
+ return req.locals._json;
157
+ };
158
+ const cookiesFn = (): SimpleStorage => {
159
+ if (!req.locals._cookies) {
160
+ req.locals._cookies = new SimpleStorage(req.headers ? parseCookies(req.headers.cookie) : {});
161
+ }
162
+ return req.locals._cookies;
163
+ };
164
+ const setCookieFn = (name: string, value: string, options: SetCookieProps): void => {
165
+ const cookies: string[] = [];
166
+ const cookiesOld = res.getHeader('Set-Cookie');
167
+ if (cookiesOld) {
168
+ if (!Array.isArray(cookiesOld)) {
169
+ cookies.push(cookiesOld as any);
170
+ } else {
171
+ cookies.push(...cookiesOld);
172
+ }
173
+ }
174
+
175
+ const cookiePair = serializeCookie(name, value, options);
176
+ cookies.push(cookiePair);
177
+ res.setHeader('Set-Cookie', cookies);
178
+
179
+ const localCookies = req.locals.cookies();
180
+ localCookies.set(name, value);
181
+ };
182
+
183
+ req.locals = {
184
+ uuid,
185
+ host,
186
+ url,
187
+ hostPath,
188
+ // urlSections: urlSplit[0].split('/').filter((i) => !!i),
189
+ query: new URLSearchParams(urlSplit[1] || ''),
190
+ urlWithoutQuery: urlSplit[0],
191
+ urlParameters: new SimpleStorage({}),
192
+ body: undefined,
193
+ json: jsonFn,
194
+ cookies: cookiesFn,
195
+ setCookie: setCookieFn,
196
+ clearCookie: (name: string) => {
197
+ res.setHeader('Set-Cookie', `${name}=; max-age=0`);
198
+ },
199
+ };
200
+
201
+ let bigRequest = false;
202
+ let totalLength = 0;
203
+ const bodyData: any[] = [];
204
+ req.on('error', (err: any) => {
205
+ REQUEST_COUNT--;
206
+ logger.error(`${requestInfo}, count: ${REQUEST_COUNT}, Request Error: `, err);
207
+ handler500(res, `listener error: ${err && err.message}`);
208
+ });
209
+
210
+ req.on('data', (chunk: any) => {
211
+ totalLength += chunk.length;
212
+ logger.debug(`${requestInfo}, Request data length: ${chunk.length}, total: ${totalLength}`);
213
+ // Limit Request Size
214
+ if (!bigRequest && totalLength < MAX_REQUEST_SIZE) {
215
+ bodyData.push(chunk);
216
+ } else {
217
+ if (!bigRequest) {
218
+ bigRequest = true;
219
+ logger.warn(`Warn, request data is too big: ${totalLength} > ${MAX_REQUEST_SIZE}`);
220
+ }
221
+ req.socket.destroy();
222
+ }
223
+ });
224
+
225
+ req.on('end', async () => {
226
+ try {
227
+ if (bigRequest) {
228
+ logger.warn(`Request data is too big to process, url: ${req.locals.url}`);
229
+ handler403(res, `Request data is too big to process, url: ${req.locals.url}`);
230
+ return;
231
+ }
232
+
233
+ const body = Buffer.concat(bodyData);
234
+ const contentType = req.headers['content-type'];
235
+ logger.debug(`url: ${url}, Request body length: ${body.length}, contentType: ${contentType}`);
236
+ req.locals.body = body;
237
+
238
+ res.setHeader('Server', SERVER_NAME);
239
+ if (accessControlAllowHosts.includes(host)) {
240
+ const allowOrigin = req.headers.origin && req.headers.origin !== 'null' ? req.headers.origin : '*';
241
+ res.setHeader('Access-Control-Allow-Origin', allowOrigin);
242
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
243
+ }
244
+
245
+ const store: AsyncStorageProps = {
246
+ uuid: uuid,
247
+ hostPath: hostPath,
248
+ appName: hostPath.appName,
249
+ locals: req.locals,
250
+ lang: req.locals.cookies().get('lang', 'en') || 'en',
251
+ };
252
+ await this.processor.processRequest(store, req, res);
253
+ } finally {
254
+ REQUEST_COUNT--;
255
+ }
256
+ // await new Promise(resolve => setTimeout(resolve, 3000));
257
+
258
+ // asyncLocalStorage.run(store, async () => {
259
+ // try {
260
+ // await onEnd();
261
+ // } catch (error: any) {
262
+ // logger.error(`url: ${url}, Request end error: `, error.message);
263
+ // }
264
+
265
+ // lastRequestTime = new Date().getTime();
266
+ // const requestEnd = process.hrtime.bigint();
267
+ // REQUEST_COUNT--;
268
+ // logger.debug(
269
+ // `Request finished. Count: ${REQUEST_COUNT}, url: ${url}, time: ${new Date().toISOString()}, duration: ${Number(requestEnd - requestStart) / 1000000} ms`
270
+ // );
271
+ // });
272
+ });
273
+ }
274
+ }
@@ -3,7 +3,7 @@ import { Logger } from '../lib/logger';
3
3
  import crypto from 'crypto';
4
4
  import { parseCookies } from '../lib/utils/cookie-util';
5
5
  import { WebProcessor } from './web-processor';
6
- import { handler403, handler404, handler500, SimpleStorage } from '../api';
6
+ import { handler403, handler404, handler500, handler503, SimpleStorage } from '../api';
7
7
  import { JsonObject, AsyncStorageProps, ServerRequest, SetCookieProps } from '../models';
8
8
  import { HostToPath } from './host-to-path';
9
9
  import { serializeCookie } from '../lib/utils/cookie-util';
@@ -15,7 +15,7 @@ export const setMaxRequestSize = (size: number) => {
15
15
  };
16
16
 
17
17
  // The maximum number of requests being processed. If there are no requests for 10 minutes, this number will be reset to 0.
18
- let MAX_REQUEST_COUNT = 1024 * 1;
18
+ let MAX_REQUEST_COUNT = 100;
19
19
  let REQUEST_COUNT = 0;
20
20
  export const setMaxRequestCount = (count: number) => {
21
21
  MAX_REQUEST_COUNT = count;
@@ -39,17 +39,37 @@ export const setServerName = (serverName: string) => {
39
39
  SERVER_NAME = serverName;
40
40
  };
41
41
 
42
+ export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
43
+
42
44
  let lastRequestTime = new Date().getTime();
43
45
 
44
46
  // type ProcessRequest = (req: ServerRequest, res: ServerResponse) => void;
45
47
  export class WebListener {
48
+ // process requests before business logic, for example IP filter, rate limit, etc.
49
+ rawMiddlewares: RawMiddleware[];
46
50
  processor: WebProcessor;
47
51
 
48
52
  constructor(processRequest: WebProcessor) {
53
+ this.rawMiddlewares = [];
49
54
  this.processor = processRequest;
50
55
  }
51
56
 
52
- listener(reqOrigin: IncomingMessage, res: ServerResponse) {
57
+ addRawMiddlewareChain(middleware: RawMiddleware) {
58
+ this.rawMiddlewares.push(middleware);
59
+ }
60
+ async handleRawMiddlewares(req: IncomingMessage, res: ServerResponse) {
61
+ const runChain = (list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) => {
62
+ const dispatch = async (i: number) => {
63
+ const fn = list[i];
64
+ if (!fn) return;
65
+ await fn(context.req, context.res, () => dispatch(i + 1));
66
+ };
67
+ return dispatch(0);
68
+ };
69
+ await runChain(this.rawMiddlewares, { req, res });
70
+ }
71
+
72
+ async listener(reqOrigin: IncomingMessage, res: ServerResponse) {
53
73
  // If there is no request in the last 10 minutes, reset the request count.
54
74
  if (new Date().getTime() - lastRequestTime > 1000 * 60 * 10) {
55
75
  if (REQUEST_COUNT != 0) {
@@ -60,9 +80,15 @@ export class WebListener {
60
80
  lastRequestTime = new Date().getTime();
61
81
  }
62
82
 
83
+ // back-pressure
63
84
  if (REQUEST_COUNT > MAX_REQUEST_COUNT) {
64
85
  logger.warn(`Too many requests, count: ${REQUEST_COUNT} > ${MAX_REQUEST_COUNT}`);
65
- handler403(res, 'Too many requests');
86
+ handler503(res, 'Server is busy, please retry later.');
87
+ return;
88
+ }
89
+
90
+ await this.handleRawMiddlewares(reqOrigin, res);
91
+ if (res.writableEnded || res.headersSent) {
66
92
  return;
67
93
  }
68
94
 
@@ -78,7 +104,7 @@ export class WebListener {
78
104
  const msg = `Web root is not defined properly for host: ${host}.`;
79
105
  logger.error(msg);
80
106
  handler404(res, msg);
81
- return true;
107
+ return;
82
108
  }
83
109
 
84
110
  REQUEST_COUNT++;
@@ -4,7 +4,7 @@ import { WebListener } from './web-listener';
4
4
  import * as fs from 'fs';
5
5
  import * as http from 'http';
6
6
  import * as https from 'https';
7
- import { IncomingMessage } from 'http';
7
+ import { IncomingMessage, ServerResponse } from 'http';
8
8
  import { Duplex } from 'stream';
9
9
  import { WebProcessor } from './web-processor';
10
10
  import { DebugService } from '../api/debug-service';
@@ -23,13 +23,25 @@ export class WebServer {
23
23
  DebugService.handleUpgrade(req, socket, head);
24
24
  } else {
25
25
  logger.error(`Unexpected web socket access: ${req.url} from ${clientIp}`);
26
- socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
26
+ socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
27
27
  socket.destroy();
28
28
  }
29
29
  }
30
30
 
31
+ async listenerWrap(reqOrigin: IncomingMessage, res: ServerResponse) {
32
+ try {
33
+ await this.webListener.listener.bind(this.webListener)(reqOrigin, res);
34
+ } catch (err) {
35
+ console.error('Request error:', err);
36
+ if (!res.headersSent) {
37
+ res.statusCode = 500;
38
+ res.end('Internal Server Error');
39
+ }
40
+ }
41
+ }
42
+
31
43
  startHttp(httpPort: number, bindIp?: string, timeout?: number) {
32
- const httpServer = http.createServer(this.webListener.listener.bind(this.webListener));
44
+ const httpServer = http.createServer(this.listenerWrap.bind(this));
33
45
  if (typeof timeout === 'number') httpServer.setTimeout(timeout);
34
46
 
35
47
  httpServer.on('upgrade', this.handleUpgrade.bind(this));
@@ -69,7 +81,7 @@ export class WebServer {
69
81
  );
70
82
  }
71
83
 
72
- const httpsServer = https.createServer(httpsOptions, this.webListener.listener.bind(this.webListener));
84
+ const httpsServer = https.createServer(httpsOptions, this.listenerWrap.bind(this));
73
85
  httpsServer.on('upgrade', this.handleUpgrade.bind(this));
74
86
 
75
87
  if (typeof timeout === 'number') {
@@ -124,21 +124,21 @@ export class FsUtils {
124
124
  };
125
125
  private static getDirsFullpathDepthSub = async (dirPath: string, depth = 0, maxDepth = 1): Promise<Dirent[]> => {
126
126
  try {
127
+ const ret = [];
127
128
  const files = await fs.readdir(dirPath, {
128
129
  recursive: false,
129
130
  withFileTypes: true,
130
131
  });
132
+ ret.push(...files);
131
133
  if (depth + 1 < maxDepth) {
132
134
  for (const entry of files) {
133
135
  if (entry.isDirectory()) {
134
- if (depth < maxDepth) {
135
- const fullPath = path.join(dirPath, entry.name);
136
- (entry as any).sub = await this.getDirsFullpathDepthSub(fullPath, depth + 1, maxDepth);
137
- }
136
+ const fullPath = path.join(dirPath, entry.name);
137
+ ret.push(...(await this.getDirsFullpathDepthSub(fullPath, depth + 1, maxDepth)));
138
138
  }
139
139
  }
140
140
  }
141
- return files;
141
+ return ret;
142
142
  } catch {
143
143
  return [];
144
144
  }
@@ -17,7 +17,7 @@ export class IsType {
17
17
 
18
18
  static isObject(input: any) {
19
19
  // null is not object
20
- return input != null && Object.prototype.toString.call(input) === '[object Object]';
20
+ return input !== null && Object.prototype.toString.call(input) === '[object Object]';
21
21
  }
22
22
 
23
23
  static isObjectEmpty(obj: any) {