lupine.api 1.1.49 → 1.1.51
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/admin/admin-release.tsx +4 -1
- package/admin/admin-resources.tsx +18 -21
- package/dev/cp-index-html.js +5 -5
- package/package.json +1 -1
- package/src/admin-api/admin-api.ts +3 -0
- package/src/admin-api/admin-auth.ts +5 -0
- package/src/admin-api/admin-csv.ts +17 -4
- package/src/admin-api/admin-release.ts +29 -1
- package/src/admin-api/web-config-api.ts +19 -0
- package/src/api/api-shared-storage.ts +2 -2
- package/src/api/server-render.ts +7 -8
- package/src/app/app-message.ts +42 -10
- package/src/app/app-shared-storage.ts +32 -7
- package/src/app/app-start.ts +3 -3
- package/src/app/cleanup-exit.ts +1 -1
- package/src/app/web-listener.ts +49 -11
- package/src/lib/db/db-sqlite.ts +1 -1
- package/src/lib/db/db.ts +24 -3
- package/src/lib/logger.ts +20 -13
- package/src/models/app-shared-storage-props.ts +1 -1
- package/src/app/web-listener copy.ts +0 -274
package/admin/admin-release.tsx
CHANGED
|
@@ -218,7 +218,7 @@ export const AdminReleasePage = () => {
|
|
|
218
218
|
releaseUpdateBtn.disabled = false;
|
|
219
219
|
if (!dataResponse || dataResponse.status !== 'ok') {
|
|
220
220
|
NotificationMessage.sendMessage(
|
|
221
|
-
dataResponse.message || 'Failed to update release (timeout, possibly backend is runing, please wait!)',
|
|
221
|
+
dataResponse.message || 'Failed to update release (timeout, possibly backend is runing, please wait and click Check!)',
|
|
222
222
|
NotificationColor.Error
|
|
223
223
|
);
|
|
224
224
|
return;
|
|
@@ -259,6 +259,9 @@ export const AdminReleasePage = () => {
|
|
|
259
259
|
|
|
260
260
|
domUpdate.value = <ReleaseList result={result} onUpdate={onUpdate} onLogClick={onLogClick} />;
|
|
261
261
|
domLog.value = <pre>{JSON.stringify(result, null, 2)}</pre>;
|
|
262
|
+
if (result.releaseProgress) {
|
|
263
|
+
NotificationMessage.sendMessage('Release progress: ' + result.releaseProgress, NotificationColor.Warning);
|
|
264
|
+
}
|
|
262
265
|
};
|
|
263
266
|
|
|
264
267
|
const onRefreshCacheLocal = async () => {
|
|
@@ -30,11 +30,10 @@ const ResourcesList = (props: {
|
|
|
30
30
|
const listFolder = (results: any, parentFolder = '') => {
|
|
31
31
|
return results.map((result: any) => (
|
|
32
32
|
<div>
|
|
33
|
-
<div
|
|
34
|
-
class=
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<label class='label mr-m f-name'>{result.name}</label>
|
|
33
|
+
<div class={`row-box mt-m`}>
|
|
34
|
+
<label class='label mr-m f-name' onClick={() => props.onListFolder(parentFolder + result.name)}>
|
|
35
|
+
{result.name}
|
|
36
|
+
</label>
|
|
38
37
|
<label class='label mr-m f-time'>{result.time}</label>
|
|
39
38
|
<label class='label mr-m f-size'>{typeof result.size !== 'undefined' && formatBytes(result.size)}</label>
|
|
40
39
|
{typeof result.size !== 'undefined' && (
|
|
@@ -97,12 +96,10 @@ const ResourcesList = (props: {
|
|
|
97
96
|
));
|
|
98
97
|
};
|
|
99
98
|
const css: CssProps = {
|
|
100
|
-
'.f-folder': {
|
|
101
|
-
textDecoration: 'underline',
|
|
102
|
-
cursor: 'pointer',
|
|
103
|
-
},
|
|
104
99
|
'.f-name': {
|
|
100
|
+
textDecoration: 'underline',
|
|
105
101
|
width: '200px',
|
|
102
|
+
cursor: 'pointer',
|
|
106
103
|
},
|
|
107
104
|
'.f-size span, .f-time span': {
|
|
108
105
|
color: 'red',
|
|
@@ -154,18 +151,18 @@ export const AdminResourcesPage = () => {
|
|
|
154
151
|
fDom.click();
|
|
155
152
|
};
|
|
156
153
|
const onUploadLocal = (fPath: string) => {
|
|
157
|
-
MessageBox.show({
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
154
|
+
// MessageBox.show({
|
|
155
|
+
// title: 'Override remote file',
|
|
156
|
+
// buttonType: MessageBoxButtonProps.YesNo,
|
|
157
|
+
// contentMinWidth: '300px',
|
|
158
|
+
// handleClicked: (index: number, close) => {
|
|
159
|
+
// if (index === 0) {
|
|
160
|
+
uploadFile(fPath);
|
|
161
|
+
// }
|
|
162
|
+
// close();
|
|
163
|
+
// },
|
|
164
|
+
// children: <div>Do you upload local files that may overwrite remote file [{fPath}]?</div>,
|
|
165
|
+
// });
|
|
169
166
|
};
|
|
170
167
|
const onNewFolder = async (fPath: string) => {
|
|
171
168
|
let newName = '';
|
package/dev/cp-index-html.js
CHANGED
|
@@ -37,7 +37,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
37
37
|
// if isMobile=true, then the last number is 1, or if isMobile=false, the last is 2
|
|
38
38
|
const chgTime = Math.trunc(f1.mtime.getTime() / 10) * 10 + (isMobile ? 1 : 2);
|
|
39
39
|
|
|
40
|
-
// when it's isMobile, need to update env and configs
|
|
40
|
+
// when it's isMobile, need to update env and configs => no configs as mobile app fetches it from api
|
|
41
41
|
if (!f2 || f2.mtime.getTime() !== chgTime || isMobile) {
|
|
42
42
|
const inHtml = await fs.readFile(htmlFile, 'utf-8');
|
|
43
43
|
let outStr = inHtml.replace(/{hash}/gi, new Date().getTime().toString(36));
|
|
@@ -48,7 +48,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
48
48
|
const metaIndexStart = inHtml.indexOf(metaTextStart);
|
|
49
49
|
const metaIndexEnd = inHtml.indexOf(metaTextEnd);
|
|
50
50
|
|
|
51
|
-
const webConfig = await readWebConfig(outdirData);
|
|
51
|
+
// const webConfig = await readWebConfig(outdirData);
|
|
52
52
|
const webEnvData = webEnv.getWebEnv(appName);
|
|
53
53
|
|
|
54
54
|
outStr =
|
|
@@ -56,9 +56,9 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
56
56
|
'<script id="web-env" type="application/json">' +
|
|
57
57
|
JSON.stringify(webEnvData) +
|
|
58
58
|
'</script>\r\n' +
|
|
59
|
-
'<script id="web-setting" type="application/json">' +
|
|
60
|
-
JSON.stringify(webConfig) +
|
|
61
|
-
'</script>' +
|
|
59
|
+
// '<script id="web-setting" type="application/json">' +
|
|
60
|
+
// JSON.stringify(webConfig) +
|
|
61
|
+
// '</script>' +
|
|
62
62
|
outStr.substring(metaIndexEnd + metaTextEnd.length);
|
|
63
63
|
// outStr = webEnv.replaceWebEnv(inHtml, appName, true);
|
|
64
64
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { AdminConfig } from './admin-config';
|
|
|
11
11
|
import { Logger } from '../lib';
|
|
12
12
|
import { IApiBase, ServerRequest } from '../models';
|
|
13
13
|
import { ApiRouter } from '../api';
|
|
14
|
+
import { readWebConfig } from './web-config-api';
|
|
14
15
|
|
|
15
16
|
const logger = new Logger('admin-api');
|
|
16
17
|
|
|
@@ -27,6 +28,8 @@ export class AdminApi implements IApiBase {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
protected mountDashboard() {
|
|
31
|
+
this.router.use('/web-config', readWebConfig);
|
|
32
|
+
|
|
30
33
|
const adminDb = new AdminDb();
|
|
31
34
|
this.router.use('/db', needDevAdminSession, adminDb.getRouter());
|
|
32
35
|
|
|
@@ -10,6 +10,11 @@ export const needDevAdminSession = async (req: ServerRequest, res: ServerRespons
|
|
|
10
10
|
const devAdminSession = await adminApiHelper.getDevAdminFromCookie(req, res, true);
|
|
11
11
|
if (!devAdminSession) {
|
|
12
12
|
// return true to skip the rest of the middleware
|
|
13
|
+
const response = {
|
|
14
|
+
status: 'error',
|
|
15
|
+
message: langHelper.getLang('shared:permission_denied'),
|
|
16
|
+
};
|
|
17
|
+
ApiHelper.sendJson(req, res, response);
|
|
13
18
|
return true;
|
|
14
19
|
}
|
|
15
20
|
return false;
|
|
@@ -54,6 +54,8 @@ export const loadCSV = async (db: Db, lines: string[]) => {
|
|
|
54
54
|
let table = '';
|
|
55
55
|
let insSql = '';
|
|
56
56
|
const result: any = {};
|
|
57
|
+
let toFields: string[] = [];
|
|
58
|
+
let toFieldsIndex: number[] = [];
|
|
57
59
|
for (const i in lines) {
|
|
58
60
|
if (!lines[i] || lines[i].startsWith('#')) {
|
|
59
61
|
continue;
|
|
@@ -61,14 +63,25 @@ export const loadCSV = async (db: Db, lines: string[]) => {
|
|
|
61
63
|
if (lines[i].startsWith('@TABLE,')) {
|
|
62
64
|
table = lines[i].substring(7);
|
|
63
65
|
result[table] = { succeeded: 0, failed: 0, errorMessage: [] };
|
|
66
|
+
|
|
67
|
+
const toTableInfo = await db.getTableInfo(table);
|
|
68
|
+
toFields = toTableInfo.map((item: any) => item.name);
|
|
64
69
|
} else if (lines[i].startsWith('@FIELD,')) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
const fromFields = lines[i]
|
|
71
|
+
.substring(7)
|
|
72
|
+
.split(',')
|
|
73
|
+
.filter((item: string) => toFields.includes(item));
|
|
74
|
+
toFieldsIndex = toFields.map((item: string) => fromFields.indexOf(item));
|
|
75
|
+
const values = Array(fromFields.length).fill('?').join(',');
|
|
76
|
+
insSql = `INSERT INTO ${table} (${fromFields.join(',')} ) VALUES (${values})`;
|
|
68
77
|
} else {
|
|
78
|
+
if (toFields.length === 0 || !insSql) {
|
|
79
|
+
throw new Error('Invalid CSV format (no @TABLE or @FIELD)');
|
|
80
|
+
}
|
|
69
81
|
try {
|
|
70
82
|
const row = JSON.parse(lines[i]);
|
|
71
|
-
const
|
|
83
|
+
const values = toFieldsIndex.map((index: number) => row[index]);
|
|
84
|
+
await db.execute(insSql, values);
|
|
72
85
|
result[table].succeeded++;
|
|
73
86
|
} catch (error: any) {
|
|
74
87
|
result[table].failed++;
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
FsUtils,
|
|
11
11
|
adminApiHelper,
|
|
12
12
|
processRefreshCache,
|
|
13
|
+
apiStorage,
|
|
13
14
|
} from 'lupine.api';
|
|
14
15
|
import path from 'path';
|
|
15
16
|
import { needDevAdminSession } from './admin-auth';
|
|
16
17
|
import { adminTokenHelper } from './admin-token-helper';
|
|
17
18
|
|
|
19
|
+
const releaseProgress = 'admin-release-progress';
|
|
18
20
|
export class AdminRelease implements IApiBase {
|
|
19
21
|
private logger = new Logger('release-api');
|
|
20
22
|
protected router = new ApiRouter();
|
|
@@ -30,7 +32,7 @@ export class AdminRelease implements IApiBase {
|
|
|
30
32
|
protected mountDashboard() {
|
|
31
33
|
// called by FE
|
|
32
34
|
this.router.use('/check', needDevAdminSession, this.check.bind(this));
|
|
33
|
-
this.router.use('/update', needDevAdminSession, this.
|
|
35
|
+
this.router.use('/update', needDevAdminSession, this.callUpdate.bind(this));
|
|
34
36
|
this.router.use('/view-log', needDevAdminSession, this.viewLog.bind(this));
|
|
35
37
|
// called online or by clients
|
|
36
38
|
this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
|
|
@@ -197,6 +199,7 @@ export class AdminRelease implements IApiBase {
|
|
|
197
199
|
// const webSub = webSubFolders.filter(i => i.isDirectory()).map(i => path.join(i.parentPath.substring(appData.webPath.length + 1), i.name).replace(/\\/g, '/')).sort();
|
|
198
200
|
|
|
199
201
|
const response = {
|
|
202
|
+
releaseProgress: await apiStorage.get(releaseProgress),
|
|
200
203
|
status: 'ok',
|
|
201
204
|
message: 'check.',
|
|
202
205
|
appsFrom: apps,
|
|
@@ -261,6 +264,24 @@ export class AdminRelease implements IApiBase {
|
|
|
261
264
|
return true;
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
async callUpdate(req: ServerRequest, res: ServerResponse) {
|
|
268
|
+
// when remote server is slow, then local update call may be timeout.
|
|
269
|
+
// so we set a flag to prevent multiple update calls
|
|
270
|
+
apiStorage.set(releaseProgress, 'update started: ' + new Date().toLocaleString());
|
|
271
|
+
let result = true;
|
|
272
|
+
try {
|
|
273
|
+
result = await this.update(req, res);
|
|
274
|
+
} catch (e: any) {
|
|
275
|
+
const response = {
|
|
276
|
+
status: 'error',
|
|
277
|
+
message: e.message,
|
|
278
|
+
};
|
|
279
|
+
ApiHelper.sendJson(req, res, response);
|
|
280
|
+
}
|
|
281
|
+
apiStorage.set(releaseProgress, undefined);
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
264
285
|
async update(req: ServerRequest, res: ServerResponse) {
|
|
265
286
|
const jsonData = req.locals.json();
|
|
266
287
|
const data = await this.chkData(jsonData, req, res, false);
|
|
@@ -391,6 +412,7 @@ export class AdminRelease implements IApiBase {
|
|
|
391
412
|
this.logger.error(`updateSendFile, not found: ${sendFile}`);
|
|
392
413
|
return { status: 'error', message: 'Client file not found: ' + sendFile };
|
|
393
414
|
}
|
|
415
|
+
apiStorage.set(releaseProgress, 'updateSendFile: ' + sendFile);
|
|
394
416
|
const fileContent = (await FsUtils.readFile(sendFile))!;
|
|
395
417
|
// const compressedContent = await new Promise<Buffer>((resolve, reject) => {
|
|
396
418
|
// zlib.gzip(fileContent, (err, buffer) => {
|
|
@@ -417,6 +439,12 @@ export class AdminRelease implements IApiBase {
|
|
|
417
439
|
fileContent.length
|
|
418
440
|
}), f: ${sendFile}`
|
|
419
441
|
);
|
|
442
|
+
apiStorage.set(
|
|
443
|
+
releaseProgress,
|
|
444
|
+
`updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
|
|
445
|
+
fileContent.length
|
|
446
|
+
}), f: ${sendFile}`
|
|
447
|
+
);
|
|
420
448
|
i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
|
|
421
449
|
const remoteData = await fetch(targetUrl + '/api/admin/release/byClientUpdate', postData);
|
|
422
450
|
const resultText = await remoteData.text();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { Logger } from '../lib';
|
|
3
|
+
import { ServerRequest } from '../models';
|
|
4
|
+
import { langHelper } from '../lang';
|
|
5
|
+
import { ApiHelper, apiStorage } from '../api';
|
|
6
|
+
|
|
7
|
+
// only used by mobile app. For web, it's injected in the html by server-side render
|
|
8
|
+
const logger = new Logger('web-cfg-api');
|
|
9
|
+
export const readWebConfig = async (req: ServerRequest, res: ServerResponse) => {
|
|
10
|
+
logger.info('readWebConfig');
|
|
11
|
+
|
|
12
|
+
const response = {
|
|
13
|
+
status: 'ok',
|
|
14
|
+
result: await apiStorage.getWebAll(),
|
|
15
|
+
message: langHelper.getLang('shared:operation_success'),
|
|
16
|
+
};
|
|
17
|
+
ApiHelper.sendJson(req, res, response);
|
|
18
|
+
return true;
|
|
19
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A persistent storage for the Api
|
|
2
|
+
* A persistent storage for the Api (stored in primary process)
|
|
3
3
|
*/
|
|
4
4
|
import { IAppSharedStorage } from '../models';
|
|
5
5
|
import { SimpleStorageDataProps } from '../models';
|
|
6
6
|
import { apiCache } from './api-cache';
|
|
7
7
|
|
|
8
|
-
// ApiSharedStorage is used in api module to store variables
|
|
8
|
+
// ApiSharedStorage is used in api module to store variables under appName scope, but is stored in one place - primary process
|
|
9
9
|
export class ApiSharedStorage {
|
|
10
10
|
private storage: IAppSharedStorage | undefined;
|
|
11
11
|
|
package/src/api/server-render.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { IToClientDelivery } from '../models/to-client-delivery-props';
|
|
|
9
9
|
import { JsonObject } from '../models/json-object';
|
|
10
10
|
import { getTemplateCache } from './api-cache';
|
|
11
11
|
import { apiStorage } from './api-shared-storage';
|
|
12
|
-
import { SimpleStorageDataProps } from '../models';
|
|
13
12
|
import { RuntimeRequire } from '../lib/runtime-require';
|
|
14
13
|
|
|
15
14
|
const logger = new Logger('StaticServer');
|
|
@@ -159,13 +158,13 @@ export const serverSideRenderPage = async (
|
|
|
159
158
|
const _lupineJs = cachedHtml[nearRoot]._lupineJs;
|
|
160
159
|
const currentCache = cachedHtml[nearRoot] as CachedHtmlProps;
|
|
161
160
|
const webSetting = await apiStorage.getWebAll();
|
|
162
|
-
const webSettingShortKey: SimpleStorageDataProps = {};
|
|
163
|
-
for (let item of Object.keys(webSetting)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
161
|
+
// const webSettingShortKey: SimpleStorageDataProps = {};
|
|
162
|
+
// for (let item of Object.keys(webSetting)) {
|
|
163
|
+
// const newItem = item.substring(4);
|
|
164
|
+
// webSettingShortKey[newItem] = webSetting[item];
|
|
165
|
+
// }
|
|
167
166
|
// const webSetting = AppConfig.get(AppConfig.WEB_SETTINGS_KEY) || {};
|
|
168
|
-
const clientDelivery = new ToClientDelivery(currentCache.webEnv,
|
|
167
|
+
const clientDelivery = new ToClientDelivery(currentCache.webEnv, webSetting, req.locals.cookies());
|
|
169
168
|
const page = await _lupineJs.generatePage(props, clientDelivery);
|
|
170
169
|
// console.log(`=========load lupin: `, content);
|
|
171
170
|
|
|
@@ -199,7 +198,7 @@ export const serverSideRenderPage = async (
|
|
|
199
198
|
res.write(page.metaData);
|
|
200
199
|
res.write(page.globalCss);
|
|
201
200
|
res.write('<script id="web-env" type="application/json">' + JSON.stringify(currentCache.webEnv) + '</script>');
|
|
202
|
-
res.write('<script id="web-setting" type="application/json">' + JSON.stringify(
|
|
201
|
+
res.write('<script id="web-setting" type="application/json">' + JSON.stringify(webSetting) + '</script>');
|
|
203
202
|
res.write(
|
|
204
203
|
currentCache.content.substring(
|
|
205
204
|
currentCache.metaIndexEnd + metaTextEnd.length,
|
package/src/app/app-message.ts
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import cluster from 'cluster';
|
|
2
2
|
import { Logger, LogWriter, LogWriterMessageId } from '../lib';
|
|
3
3
|
import { processDebugMessage } from './process-dev-requests';
|
|
4
|
-
import { appStorage } from './app-shared-storage';
|
|
5
|
-
import { AppSharedStorageMessageId } from '../models';
|
|
6
4
|
import { cleanupAndExit } from './cleanup-exit';
|
|
7
5
|
|
|
6
|
+
export type AppMessageProps = {
|
|
7
|
+
id: string;
|
|
8
|
+
message: string;
|
|
9
|
+
appName?: string;
|
|
10
|
+
[id: string]: any;
|
|
11
|
+
};
|
|
12
|
+
const _savedMessageHandler: {
|
|
13
|
+
fromPrimary: { [messageId: string]: (msgObject: AppMessageProps) => void };
|
|
14
|
+
fromWorker: { [messageId: string]: (msgObject: AppMessageProps) => void };
|
|
15
|
+
} = {
|
|
16
|
+
fromPrimary: {},
|
|
17
|
+
fromWorker: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const registerMessageHandlerFromPrimary = (messageId: string, handler: (msgObject: AppMessageProps) => void) => {
|
|
21
|
+
_savedMessageHandler.fromPrimary[messageId] = handler;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const registerMessageHandlerFromWorker = (messageId: string, handler: (msgObject: AppMessageProps) => void) => {
|
|
25
|
+
_savedMessageHandler.fromWorker[messageId] = handler;
|
|
26
|
+
};
|
|
27
|
+
|
|
8
28
|
const logger = new Logger('app-message');
|
|
9
29
|
// send msg to all clients
|
|
10
|
-
const broadcast = (msgObject:
|
|
30
|
+
const broadcast = (msgObject: AppMessageProps) => {
|
|
11
31
|
for (let i in cluster.workers) {
|
|
12
32
|
if (cluster.workers[i]) cluster.workers[i].send(msgObject);
|
|
13
33
|
}
|
|
@@ -15,33 +35,45 @@ const broadcast = (msgObject: any) => {
|
|
|
15
35
|
|
|
16
36
|
// this is a worker and msg is from Primary
|
|
17
37
|
// when debug is on, it's in primary, but it shouldn't receive those msgs
|
|
18
|
-
export const processMessageFromPrimary = (msgObject:
|
|
38
|
+
export const processMessageFromPrimary = (msgObject: AppMessageProps) => {
|
|
19
39
|
if (!msgObject || !msgObject.id) {
|
|
20
40
|
logger.warn(`Unknown message from master in work: ${cluster.worker?.id}`);
|
|
21
41
|
return;
|
|
22
42
|
}
|
|
23
43
|
|
|
44
|
+
const primaryFn = _savedMessageHandler.fromPrimary[msgObject.id];
|
|
45
|
+
if (primaryFn) {
|
|
46
|
+
primaryFn(msgObject);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
if (msgObject.id == 'debug') {
|
|
25
51
|
processDebugMessage(msgObject);
|
|
26
|
-
|
|
27
|
-
appStorage.messageFromPrimaryProcess(msgObject);
|
|
52
|
+
// } else if (msgObject.id == AppSharedStorageMessageId) {
|
|
53
|
+
// appStorage.messageFromPrimaryProcess(msgObject);
|
|
28
54
|
} else {
|
|
29
55
|
logger.warn(`Unknown message: ${msgObject.id}`);
|
|
30
56
|
}
|
|
31
57
|
};
|
|
32
58
|
|
|
33
59
|
// this is primary, msg is from a client
|
|
34
|
-
export const processMessageFromWorker = (msgObject:
|
|
60
|
+
export const processMessageFromWorker = (msgObject: AppMessageProps) => {
|
|
35
61
|
if (!msgObject || !msgObject.id) {
|
|
36
62
|
if (msgObject['watch:require']) return;
|
|
37
63
|
logger.warn(`Unknown message from work: ${cluster.worker?.id}`);
|
|
38
64
|
return;
|
|
39
65
|
}
|
|
40
66
|
|
|
67
|
+
const workerFn = _savedMessageHandler.fromWorker[msgObject.id];
|
|
68
|
+
if (workerFn) {
|
|
69
|
+
workerFn(msgObject);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
41
73
|
if (msgObject.id == LogWriterMessageId) {
|
|
42
|
-
LogWriter.messageFromSubProcess(msgObject);
|
|
43
|
-
|
|
44
|
-
appStorage.messageFromSubProcess(msgObject);
|
|
74
|
+
LogWriter.messageFromSubProcess(msgObject as any);
|
|
75
|
+
// } else if (msgObject.id == AppSharedStorageMessageId) {
|
|
76
|
+
// appStorage.messageFromSubProcess(msgObject);
|
|
45
77
|
} else if (msgObject.id == 'debug') {
|
|
46
78
|
logger.debug(
|
|
47
79
|
`Message from worker ${cluster.worker?.id}, message: ${msgObject.message}, appName: ${msgObject.appName}`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A persistent storage
|
|
2
|
+
* A persistent storage to store data in primary process and share to all workers
|
|
3
|
+
*
|
|
4
|
+
* You should use apiStorage in api module
|
|
3
5
|
*/
|
|
4
6
|
import * as fs from 'fs/promises';
|
|
5
7
|
import * as path from 'path';
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
IAppSharedStorage,
|
|
15
17
|
StorageMessageFromSubProcess,
|
|
16
18
|
} from '../models';
|
|
19
|
+
import { registerMessageHandlerFromPrimary, registerMessageHandlerFromWorker } from './app-message';
|
|
17
20
|
|
|
18
21
|
// in Api scope, use ApiSharedStorage instead of this
|
|
19
22
|
// storage cross clusters, loaded when start and saved before exist
|
|
@@ -27,6 +30,15 @@ export class AppSharedStorage implements IAppSharedStorage {
|
|
|
27
30
|
public static getInstance(): IAppSharedStorage {
|
|
28
31
|
if (!AppSharedStorage.instance) {
|
|
29
32
|
AppSharedStorage.instance = new AppSharedStorage();
|
|
33
|
+
// register app message handlers
|
|
34
|
+
registerMessageHandlerFromPrimary(
|
|
35
|
+
AppSharedStorageMessageId,
|
|
36
|
+
AppSharedStorage.instance.messageFromPrimaryProcess.bind(AppSharedStorage.instance)
|
|
37
|
+
);
|
|
38
|
+
registerMessageHandlerFromWorker(
|
|
39
|
+
AppSharedStorageMessageId,
|
|
40
|
+
AppSharedStorage.instance.messageFromSubProcess.bind(AppSharedStorage.instance)
|
|
41
|
+
);
|
|
30
42
|
}
|
|
31
43
|
return AppSharedStorage.instance;
|
|
32
44
|
}
|
|
@@ -129,9 +141,9 @@ export class AppSharedStorage implements IAppSharedStorage {
|
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
// called from primary before exit, or from api to save changes
|
|
132
|
-
async save(appName?: string) {
|
|
144
|
+
async save(appName?: string, exit?: boolean) {
|
|
133
145
|
if (!cluster.isPrimary) {
|
|
134
|
-
AppSharedStorageWorker.save(appName);
|
|
146
|
+
await AppSharedStorageWorker.save(appName);
|
|
135
147
|
return;
|
|
136
148
|
}
|
|
137
149
|
|
|
@@ -171,8 +183,16 @@ export class AppSharedStorage implements IAppSharedStorage {
|
|
|
171
183
|
getApi(appName: string, key: string): Promise<string> {
|
|
172
184
|
return this.get(appName, AppSharedStorageApiPrefix + key);
|
|
173
185
|
}
|
|
174
|
-
getWebAll(appName: string): Promise<SimpleStorageDataProps> {
|
|
175
|
-
|
|
186
|
+
async getWebAll(appName: string): Promise<SimpleStorageDataProps> {
|
|
187
|
+
const webAll = await this.getWithPrefix(appName, AppSharedStorageWebPrefix);
|
|
188
|
+
|
|
189
|
+
const webSettingShortKey: SimpleStorageDataProps = {};
|
|
190
|
+
for (let item of Object.keys(webAll)) {
|
|
191
|
+
const newItem = item.substring(AppSharedStorageWebPrefix.length);
|
|
192
|
+
webSettingShortKey[newItem] = webAll[item];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return webSettingShortKey;
|
|
176
196
|
}
|
|
177
197
|
getWithPrefix(appName: string, prefixKey: string): Promise<SimpleStorageDataProps> {
|
|
178
198
|
return new Promise((resolve, reject) => {
|
|
@@ -255,7 +275,7 @@ class AppSharedStorageWorker {
|
|
|
255
275
|
workerId: cluster.worker?.id || 0,
|
|
256
276
|
action: 'save',
|
|
257
277
|
appName: appName || '',
|
|
258
|
-
key: '',
|
|
278
|
+
key: 'save',
|
|
259
279
|
};
|
|
260
280
|
process.send!(obj);
|
|
261
281
|
}
|
|
@@ -278,7 +298,12 @@ class AppSharedStorageWorker {
|
|
|
278
298
|
process.send!(obj);
|
|
279
299
|
}
|
|
280
300
|
|
|
281
|
-
static getWithPrefix(
|
|
301
|
+
static getWithPrefix(
|
|
302
|
+
appName: string,
|
|
303
|
+
prefixKey: string,
|
|
304
|
+
resolve: (value: any) => void,
|
|
305
|
+
reject: (reason: any) => void
|
|
306
|
+
) {
|
|
282
307
|
if (cluster.isPrimary) {
|
|
283
308
|
throw new Error('AppSharedStorageWorker should be only called from workers');
|
|
284
309
|
}
|
package/src/app/app-start.ts
CHANGED
|
@@ -46,7 +46,7 @@ class AppStart {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (
|
|
49
|
+
if (!cluster.isPrimary) {
|
|
50
50
|
console.log(`Worker id ${this.getWorkerId()}`);
|
|
51
51
|
|
|
52
52
|
process.on('message', processMessageFromPrimary);
|
|
@@ -55,7 +55,7 @@ class AppStart {
|
|
|
55
55
|
appLoader.loadApi(props.apiConfig);
|
|
56
56
|
this.initServer(props.serverConfig);
|
|
57
57
|
} else if (cluster.isPrimary) {
|
|
58
|
-
const numCPUs = require('os').cpus().length;
|
|
58
|
+
const numCPUs = props.debug ? 1 : require('os').cpus().length;
|
|
59
59
|
console.log(`Master Process is trying to fork ${numCPUs} processes`);
|
|
60
60
|
|
|
61
61
|
for (let i = 0; i < numCPUs; i++) {
|
|
@@ -73,7 +73,7 @@ class AppStart {
|
|
|
73
73
|
bindProcess() {
|
|
74
74
|
if (cluster.isPrimary) {
|
|
75
75
|
// it looks like the child processes are hung up here
|
|
76
|
-
process.stdin.resume(); // so the program will not close instantly
|
|
76
|
+
process.stdin.resume(); // so the program will not close instantly, keep isPrimary process running
|
|
77
77
|
}
|
|
78
78
|
// Emitted whenever a no-error-handler Promise is rejected
|
|
79
79
|
process.on('unhandledRejection', (reason: string, promise) => {
|
package/src/app/cleanup-exit.ts
CHANGED
package/src/app/web-listener.ts
CHANGED
|
@@ -39,7 +39,47 @@ 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
|
|
42
|
+
export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => Promise<void>;
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
IP limit
|
|
46
|
+
|
|
47
|
+
let IP_LIMIT_ENABLED = false;
|
|
48
|
+
export const setIpLimitEnabled = (enabled: boolean) => {
|
|
49
|
+
IP_LIMIT_ENABLED = enabled;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let IP_LIMIT_RATE = 60;
|
|
53
|
+
let IP_LIMIT_DURATION = 1000 * 30;
|
|
54
|
+
export const setIpLimitRateAndDuration = (rate: number, durationSeconds: number) => {
|
|
55
|
+
IP_LIMIT_RATE = rate;
|
|
56
|
+
IP_LIMIT_DURATION = durationSeconds * 1000;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TTLMap<K, V> extends Map<K, V> {
|
|
61
|
+
set(key: K, value: V): this;
|
|
62
|
+
set(key: K, value: V, ttl: number): this;
|
|
63
|
+
set(key: K, value: V, ttl?: number): this {
|
|
64
|
+
const isNew = !this.has(key);
|
|
65
|
+
super.set(key, value);
|
|
66
|
+
if (isNew && ttl) {
|
|
67
|
+
setTimeout(() => this.delete(key), ttl).unref?.();
|
|
68
|
+
}
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const counts = new TTLMap<string, number>();
|
|
73
|
+
|
|
74
|
+
function checkFixedWindow(ip: string, limit = IP_LIMIT_RATE) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const key = `${ip}:${Math.floor(now / IP_LIMIT_DURATION)}`;
|
|
77
|
+
const count = (counts.get(key) || 0) + 1;
|
|
78
|
+
counts.set(key, count, IP_LIMIT_DURATION * 2); // key自动过期
|
|
79
|
+
return count > limit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
*/
|
|
43
83
|
|
|
44
84
|
let lastRequestTime = new Date().getTime();
|
|
45
85
|
|
|
@@ -57,16 +97,14 @@ export class WebListener {
|
|
|
57
97
|
addRawMiddlewareChain(middleware: RawMiddleware) {
|
|
58
98
|
this.rawMiddlewares.push(middleware);
|
|
59
99
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
67
|
-
return dispatch(0);
|
|
100
|
+
|
|
101
|
+
runMiddlewareChain(list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) {
|
|
102
|
+
const dispatch = async (i: number) => {
|
|
103
|
+
const fn = list[i];
|
|
104
|
+
if (!fn) return;
|
|
105
|
+
await fn(context.req, context.res, () => dispatch(i + 1));
|
|
68
106
|
};
|
|
69
|
-
|
|
107
|
+
return dispatch(0);
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
async listener(reqOrigin: IncomingMessage, res: ServerResponse) {
|
|
@@ -87,7 +125,7 @@ export class WebListener {
|
|
|
87
125
|
return;
|
|
88
126
|
}
|
|
89
127
|
|
|
90
|
-
await this.
|
|
128
|
+
await this.runMiddlewareChain(this.rawMiddlewares, { req: reqOrigin, res });
|
|
91
129
|
if (res.writableEnded || res.headersSent) {
|
|
92
130
|
return;
|
|
93
131
|
}
|
package/src/lib/db/db-sqlite.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class DbSqlite extends Db {
|
|
|
95
95
|
|
|
96
96
|
public async getTableInfo(table: string): Promise<any> {
|
|
97
97
|
const query = `PRAGMA table_info(${table});`;
|
|
98
|
-
const result = await this.
|
|
98
|
+
const result = await this.select(query);
|
|
99
99
|
return result;
|
|
100
100
|
}
|
|
101
101
|
}
|
package/src/lib/db/db.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { Logger } from '../logger';
|
|
|
2
2
|
import { DbConfig } from '../../models/db-config';
|
|
3
3
|
|
|
4
4
|
// Instead, Boolean values are stored as integers 0 (false) and 1 (true).
|
|
5
|
+
export type DbFieldExprssionProps = { exprssion: string; params?: (string | number)[] };
|
|
5
6
|
export type DbFieldValue = { [key: string]: string | number };
|
|
7
|
+
export type DbFieldExpression = { [key: string]: string | number | DbFieldExprssionProps };
|
|
8
|
+
const isDbFieldExprssion = (value: any): value is DbFieldExprssionProps => {
|
|
9
|
+
return value && typeof value === 'object' && 'exprssion' in value;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
const logger = new Logger('db');
|
|
8
13
|
export class Db {
|
|
@@ -252,12 +257,28 @@ export class Db {
|
|
|
252
257
|
return await this.execute(sql, params);
|
|
253
258
|
}
|
|
254
259
|
|
|
255
|
-
public async updateObject(table: string, updateFieldValues:
|
|
260
|
+
public async updateObject(table: string, updateFieldValues: DbFieldExpression, whereFieldValues: DbFieldValue) {
|
|
256
261
|
table = this.replacePrefix(table);
|
|
257
262
|
const fields = Object.keys(updateFieldValues);
|
|
258
|
-
|
|
259
|
-
const params =
|
|
263
|
+
const setClauseParts: string[] = [];
|
|
264
|
+
const params: (string | number)[] = [];
|
|
265
|
+
// let sql = 'UPDATE ' + table + ' SET ' + fields.map((item) => `${item}=?`).join(',');
|
|
266
|
+
// const params = Object.values(updateFieldValues);
|
|
267
|
+
for (const field of fields) {
|
|
268
|
+
const value = updateFieldValues[field];
|
|
269
|
+
|
|
270
|
+
// expression
|
|
271
|
+
if (isDbFieldExprssion(value)) {
|
|
272
|
+
setClauseParts.push(`${field} = ${value.exprssion}`);
|
|
273
|
+
if (value.params) params.push(...value.params);
|
|
274
|
+
} else {
|
|
275
|
+
// static value
|
|
276
|
+
setClauseParts.push(`${field} = ?`);
|
|
277
|
+
params.push(value);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
260
280
|
|
|
281
|
+
let sql = `UPDATE ${table} SET ${setClauseParts.join(', ')}`;
|
|
261
282
|
if (whereFieldValues && Object.keys(whereFieldValues).length > 0) {
|
|
262
283
|
sql +=
|
|
263
284
|
' WHERE ' +
|
package/src/lib/logger.ts
CHANGED
|
@@ -25,10 +25,17 @@ export const LogWriterMessageId = 'LogWriter';
|
|
|
25
25
|
logger.info('log test. numer: %d, string: %s, json: %j', 11, 'p2', {key: 'value', n: 22});
|
|
26
26
|
*/
|
|
27
27
|
export class LogWriter {
|
|
28
|
-
static instance
|
|
28
|
+
static instance?: LogWriter;
|
|
29
29
|
|
|
30
30
|
private constructor() {}
|
|
31
31
|
|
|
32
|
+
public static getInstance(): LogWriter {
|
|
33
|
+
if (!LogWriter.instance) {
|
|
34
|
+
LogWriter.instance = new LogWriter();
|
|
35
|
+
}
|
|
36
|
+
return LogWriter.instance;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
private _config: LogConfig | undefined;
|
|
33
40
|
getConfig = (): LogConfig => {
|
|
34
41
|
return this._config || getDefaultLogConfig();
|
|
@@ -138,7 +145,7 @@ export class LogWriter {
|
|
|
138
145
|
console.error('Logger got wrong message: ', msgObject);
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
|
-
LogWriter.
|
|
148
|
+
LogWriter.getInstance()._log(
|
|
142
149
|
msgObject.level,
|
|
143
150
|
msgObject.namespace,
|
|
144
151
|
msgObject.color,
|
|
@@ -234,41 +241,41 @@ export class Logger {
|
|
|
234
241
|
}
|
|
235
242
|
|
|
236
243
|
isDebug(): boolean {
|
|
237
|
-
return LogWriter.
|
|
244
|
+
return LogWriter.getInstance().level <= 4;
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
debug(...message: (string | object | number)[]) {
|
|
241
|
-
if (LogWriter.
|
|
248
|
+
if (LogWriter.getInstance().level < 4) {
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
244
|
-
LogWriter.
|
|
251
|
+
LogWriter.getInstance()._log('DEBUG', this.namespace, this.color, message);
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
info(...message: (string | object | number)[]) {
|
|
248
|
-
if (LogWriter.
|
|
255
|
+
if (LogWriter.getInstance().level < 3) {
|
|
249
256
|
return;
|
|
250
257
|
}
|
|
251
|
-
LogWriter.
|
|
258
|
+
LogWriter.getInstance()._log('INFO', this.namespace, this.color, message);
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
warn(...message: (string | object | number)[]) {
|
|
255
|
-
if (LogWriter.
|
|
262
|
+
if (LogWriter.getInstance().level < 2) {
|
|
256
263
|
return;
|
|
257
264
|
}
|
|
258
|
-
LogWriter.
|
|
265
|
+
LogWriter.getInstance()._log('WARN', this.namespace, this.color, message);
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
error(...message: (string | object | number)[]) {
|
|
262
|
-
if (LogWriter.
|
|
269
|
+
if (LogWriter.getInstance().level < 1) {
|
|
263
270
|
return;
|
|
264
271
|
}
|
|
265
|
-
LogWriter.
|
|
272
|
+
LogWriter.getInstance()._log('ERROR', this.namespace, this.color, message);
|
|
266
273
|
}
|
|
267
274
|
|
|
268
275
|
fatal(...message: (string | object | number)[]) {
|
|
269
|
-
if (LogWriter.
|
|
276
|
+
if (LogWriter.getInstance().level < 0) {
|
|
270
277
|
return;
|
|
271
278
|
}
|
|
272
|
-
LogWriter.
|
|
279
|
+
LogWriter.getInstance()._log('FATAL', this.namespace, this.color, message);
|
|
273
280
|
}
|
|
274
281
|
}
|
|
@@ -25,7 +25,7 @@ export interface IAppSharedStorage {
|
|
|
25
25
|
// this is primary, msg is from a client
|
|
26
26
|
messageFromSubProcess(msgObject: any): void;
|
|
27
27
|
load(appName: string, rootPath: string): Promise<void>;
|
|
28
|
-
save(appName?: string): Promise<void>;
|
|
28
|
+
save(appName?: string, exit?: boolean): Promise<void>;
|
|
29
29
|
get(appName: string, key: string): Promise<string>;
|
|
30
30
|
getWeb(appName: string, key: string): Promise<string>;
|
|
31
31
|
getApi(appName: string, key: string): Promise<string>;
|
|
@@ -1,274 +0,0 @@
|
|
|
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
|
-
}
|