lupine.api 1.0.41
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/LICENSE +21 -0
- package/README.md +3 -0
- package/admin/admin-about.tsx +16 -0
- package/admin/admin-config.tsx +44 -0
- package/admin/admin-css.tsx +3 -0
- package/admin/admin-db.tsx +74 -0
- package/admin/admin-frame-props.tsx +9 -0
- package/admin/admin-frame.tsx +466 -0
- package/admin/admin-index.tsx +66 -0
- package/admin/admin-login.tsx +99 -0
- package/admin/admin-menu-edit.tsx +637 -0
- package/admin/admin-menu-list.tsx +87 -0
- package/admin/admin-page-edit.tsx +564 -0
- package/admin/admin-page-list.tsx +83 -0
- package/admin/admin-performance.tsx +28 -0
- package/admin/admin-release.tsx +320 -0
- package/admin/admin-resources.tsx +385 -0
- package/admin/admin-shell.tsx +89 -0
- package/admin/admin-table-data.tsx +146 -0
- package/admin/admin-table-list.tsx +231 -0
- package/admin/admin-test-animations.tsx +379 -0
- package/admin/admin-test-component.tsx +808 -0
- package/admin/admin-test-edit.tsx +319 -0
- package/admin/admin-test-themes.tsx +56 -0
- package/admin/admin-tokens.tsx +338 -0
- package/admin/design/admin-design.tsx +174 -0
- package/admin/design/block-grid.tsx +36 -0
- package/admin/design/block-grid1.tsx +21 -0
- package/admin/design/block-paragraph.tsx +19 -0
- package/admin/design/block-title.tsx +19 -0
- package/admin/design/design-block-box.tsx +140 -0
- package/admin/design/drag-data.tsx +24 -0
- package/admin/index.ts +6 -0
- package/admin/package.json +15 -0
- package/admin/tsconfig.json +127 -0
- package/dev/copy-folder.js +32 -0
- package/dev/cp-index-html.js +69 -0
- package/dev/file-utils.js +12 -0
- package/dev/index.js +19 -0
- package/dev/package.json +12 -0
- package/dev/plugin-gen-versions.js +20 -0
- package/dev/plugin-ifelse.js +155 -0
- package/dev/plugin-ifelse.test.js +37 -0
- package/dev/run-cmd.js +14 -0
- package/dev/send-request.js +12 -0
- package/package.json +55 -0
- package/src/admin-api/admin-api.ts +59 -0
- package/src/admin-api/admin-auth.ts +87 -0
- package/src/admin-api/admin-config.ts +93 -0
- package/src/admin-api/admin-csv.ts +81 -0
- package/src/admin-api/admin-db.ts +269 -0
- package/src/admin-api/admin-helper.ts +111 -0
- package/src/admin-api/admin-menu.ts +135 -0
- package/src/admin-api/admin-page.ts +135 -0
- package/src/admin-api/admin-performance.ts +128 -0
- package/src/admin-api/admin-release.ts +498 -0
- package/src/admin-api/admin-resources.ts +318 -0
- package/src/admin-api/admin-token-helper.ts +79 -0
- package/src/admin-api/admin-tokens.ts +90 -0
- package/src/admin-api/index.ts +2 -0
- package/src/api/api-cache.ts +103 -0
- package/src/api/api-helper.ts +44 -0
- package/src/api/api-module.ts +60 -0
- package/src/api/api-router.ts +177 -0
- package/src/api/api-shared-storage.ts +64 -0
- package/src/api/async-storage.ts +5 -0
- package/src/api/debug-service.ts +56 -0
- package/src/api/encode-html.ts +27 -0
- package/src/api/handle-status.ts +71 -0
- package/src/api/index.ts +16 -0
- package/src/api/mini-web-socket.ts +270 -0
- package/src/api/server-content-type.ts +82 -0
- package/src/api/server-render.ts +216 -0
- package/src/api/shell-service.ts +66 -0
- package/src/api/simple-storage.ts +80 -0
- package/src/api/static-server.ts +125 -0
- package/src/api/to-client-delivery.ts +26 -0
- package/src/app/app-cache.ts +55 -0
- package/src/app/app-loader.ts +62 -0
- package/src/app/app-message.ts +60 -0
- package/src/app/app-shared-storage.ts +317 -0
- package/src/app/app-start.ts +117 -0
- package/src/app/cleanup-exit.ts +12 -0
- package/src/app/host-to-path.ts +38 -0
- package/src/app/index.ts +11 -0
- package/src/app/process-dev-requests.ts +90 -0
- package/src/app/web-listener.ts +230 -0
- package/src/app/web-processor.ts +42 -0
- package/src/app/web-server.ts +86 -0
- package/src/common-js/web-env.js +104 -0
- package/src/index.ts +7 -0
- package/src/lang/api-lang-en.ts +27 -0
- package/src/lang/api-lang-zh-cn.ts +28 -0
- package/src/lang/index.ts +2 -0
- package/src/lang/lang-helper.ts +76 -0
- package/src/lang/lang-props.ts +6 -0
- package/src/lib/db/db-helper.ts +23 -0
- package/src/lib/db/db-mysql.ts +250 -0
- package/src/lib/db/db-sqlite.ts +101 -0
- package/src/lib/db/db.spec.ts +28 -0
- package/src/lib/db/db.ts +304 -0
- package/src/lib/db/index.ts +5 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/logger.spec.ts +214 -0
- package/src/lib/logger.ts +274 -0
- package/src/lib/runtime-require.ts +37 -0
- package/src/lib/utils/cookie-util.ts +34 -0
- package/src/lib/utils/crypto.ts +58 -0
- package/src/lib/utils/date-utils.ts +317 -0
- package/src/lib/utils/deep-merge.ts +37 -0
- package/src/lib/utils/delay.ts +12 -0
- package/src/lib/utils/file-setting.ts +55 -0
- package/src/lib/utils/format-bytes.ts +11 -0
- package/src/lib/utils/fs-utils.ts +144 -0
- package/src/lib/utils/get-env.ts +27 -0
- package/src/lib/utils/index.ts +12 -0
- package/src/lib/utils/is-type.ts +48 -0
- package/src/lib/utils/load-env.ts +14 -0
- package/src/lib/utils/pad.ts +6 -0
- package/src/models/api-base.ts +5 -0
- package/src/models/api-module-props.ts +11 -0
- package/src/models/api-router-props.ts +26 -0
- package/src/models/app-cache-props.ts +33 -0
- package/src/models/app-data-props.ts +10 -0
- package/src/models/app-loader-props.ts +6 -0
- package/src/models/app-shared-storage-props.ts +37 -0
- package/src/models/app-start-props.ts +18 -0
- package/src/models/async-storage-props.ts +13 -0
- package/src/models/db-config.ts +30 -0
- package/src/models/host-to-path-props.ts +12 -0
- package/src/models/index.ts +16 -0
- package/src/models/json-object.ts +8 -0
- package/src/models/locals-props.ts +36 -0
- package/src/models/logger-props.ts +84 -0
- package/src/models/simple-storage-props.ts +14 -0
- package/src/models/to-client-delivery-props.ts +6 -0
- package/tsconfig.json +115 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { Logger } from '../lib';
|
|
3
|
+
import { ServerRequest } from '../models/locals-props';
|
|
4
|
+
import { handler404 } from './handle-status';
|
|
5
|
+
import { SimpleStorage } from './simple-storage';
|
|
6
|
+
import { ApiRouterCallback, ApiRouterData, ApiRouterMethod, IApiRouter } from '../models/api-router-props';
|
|
7
|
+
const logger = new Logger('api-router');
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
If there is a common logic to be called all endpoints, then you can set filter.
|
|
11
|
+
Or, for a particular endpoint, if you want a logic to be called before any other
|
|
12
|
+
WebRouterCallback or WebRouter it can be done like this:
|
|
13
|
+
|
|
14
|
+
const commLogic: WebRouterCallback = async (req: ServerRequest, res: ServerResponse) => {
|
|
15
|
+
console.log('this is called by all...');
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
router.use('/auth', commLogic, otherLogic);
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function isIApiRouter(handler: ApiRouterCallback | IApiRouter): handler is IApiRouter {
|
|
22
|
+
return (handler as IApiRouter).findRoute !== undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ApiRouter implements IApiRouter {
|
|
26
|
+
private routerData: ApiRouterData[] = [];
|
|
27
|
+
private filter: ApiRouterCallback | undefined;
|
|
28
|
+
|
|
29
|
+
setFilter(filter: ApiRouterCallback) {
|
|
30
|
+
this.filter = filter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// the path should start with / and end without /, and it can be
|
|
34
|
+
// /aaa/:bbb/ccc/:ddd (ccc is a fixed section)
|
|
35
|
+
// /aaa/:bbb/ccc/?ddd/?eee (from ddd, all sections are optional)
|
|
36
|
+
private storeRouter(path: string, handler: (ApiRouterCallback | IApiRouter)[], method: ApiRouterMethod) {
|
|
37
|
+
let fixedPath;
|
|
38
|
+
if (path === '*' || path === '/' || path === '' || path === '/*') {
|
|
39
|
+
fixedPath = '*';
|
|
40
|
+
} else {
|
|
41
|
+
fixedPath = path;
|
|
42
|
+
if (!fixedPath.startsWith('/')) {
|
|
43
|
+
fixedPath = '/' + fixedPath;
|
|
44
|
+
}
|
|
45
|
+
if (fixedPath.endsWith('/')) {
|
|
46
|
+
fixedPath = fixedPath.substring(0, fixedPath.length - 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let parameterLength = 0;
|
|
51
|
+
let parameterVariables: string[] = [];
|
|
52
|
+
const ind = fixedPath.indexOf('/:');
|
|
53
|
+
if (ind >= 0) {
|
|
54
|
+
parameterVariables = fixedPath.substring(ind + 1).split('/');
|
|
55
|
+
fixedPath = fixedPath.substring(0, ind);
|
|
56
|
+
// from optionInd, all will be optional
|
|
57
|
+
const optionInd = parameterVariables.findIndex((item) => item.startsWith('?'));
|
|
58
|
+
parameterLength = optionInd >= 0 ? optionInd : parameterVariables.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.routerData.push({
|
|
62
|
+
path: fixedPath,
|
|
63
|
+
handler,
|
|
64
|
+
method: method,
|
|
65
|
+
parameterVariables,
|
|
66
|
+
parameterLength,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get(path: string, ...handler: (ApiRouterCallback | IApiRouter)[]) {
|
|
71
|
+
this.storeRouter(path, handler, ApiRouterMethod.GET);
|
|
72
|
+
}
|
|
73
|
+
post(path: string, ...handler: (ApiRouterCallback | IApiRouter)[]) {
|
|
74
|
+
this.storeRouter(path, handler, ApiRouterMethod.POST);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
use(path: string, ...handler: (ApiRouterCallback | IApiRouter)[]) {
|
|
78
|
+
this.storeRouter(path, handler, ApiRouterMethod.ALL);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async callHandle(
|
|
82
|
+
handle: ApiRouterCallback,
|
|
83
|
+
path: string,
|
|
84
|
+
req: ServerRequest,
|
|
85
|
+
res: ServerResponse
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
if ((await handle(req, res, path)) || res.writableEnded) {
|
|
89
|
+
logger.debug(`Processed path: ${path}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
} catch (e: any) {
|
|
93
|
+
logger.error(`Processed path: ${path}, error: ${e.message}`);
|
|
94
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
95
|
+
res.write(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
status: 'error',
|
|
98
|
+
message: `Processed path: ${path}, error: ${e.message}`,
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
res.end();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async findRoute(url: string, req: ServerRequest, res: ServerResponse, handleNotFound: boolean): Promise<boolean> {
|
|
108
|
+
for (let i = 0, routerList; (routerList = this.routerData[i]); i++) {
|
|
109
|
+
if (
|
|
110
|
+
(routerList.method === ApiRouterMethod.ALL || routerList.method === req.method) &&
|
|
111
|
+
(routerList.path === '*' || url === routerList.path || url.startsWith(routerList.path + '/'))
|
|
112
|
+
) {
|
|
113
|
+
const parameters: { [key: string]: string } = {};
|
|
114
|
+
let meet = true;
|
|
115
|
+
if (routerList.parameterVariables.length > 0) {
|
|
116
|
+
meet = false;
|
|
117
|
+
const restPath = url.substring(routerList.path.length + 1).split('/');
|
|
118
|
+
// the path must have mandatory parameters but some parameters can be optional
|
|
119
|
+
if (
|
|
120
|
+
restPath.length >= routerList.parameterLength &&
|
|
121
|
+
restPath.length <= routerList.parameterVariables.length
|
|
122
|
+
) {
|
|
123
|
+
meet = true;
|
|
124
|
+
for (const [index, item] of routerList.parameterVariables.entries()) {
|
|
125
|
+
if (!item.startsWith(':') && !item.startsWith('?') && item !== restPath[index]) {
|
|
126
|
+
meet = false;
|
|
127
|
+
break;
|
|
128
|
+
} else if ((item.startsWith(':') || item.startsWith('?')) && index < restPath.length) {
|
|
129
|
+
parameters[item.replace(/[:?]/g, '')] = restPath[index];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
req.locals.urlParameters = new SimpleStorage(parameters);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (meet) {
|
|
137
|
+
for (let j = 0, router; (router = routerList.handler[j]); j++) {
|
|
138
|
+
if (isIApiRouter(router)) {
|
|
139
|
+
// it's a sub-level router
|
|
140
|
+
const nextPath = routerList.path === '*' ? url : url.substring(routerList.path.length);
|
|
141
|
+
// the sub-level router will not have the appName
|
|
142
|
+
if ((await router.findRoute(nextPath, req, res, handleNotFound)) || res.writableEnded) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// it should be a function
|
|
147
|
+
// the query's url should match the api's path
|
|
148
|
+
if (await this.callHandle(router, url, req, res)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (handleNotFound) {
|
|
156
|
+
// no match under this path
|
|
157
|
+
logger.debug(`Processed path: ${url}, router path: ${routerList.path}`);
|
|
158
|
+
const html = JSON.stringify({
|
|
159
|
+
status: 'error',
|
|
160
|
+
message: `Can't find any matches under router ${routerList.path} for path: ${url}.`,
|
|
161
|
+
});
|
|
162
|
+
handler404(res, html);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async handleRequest(url: string, req: ServerRequest, res: ServerResponse): Promise<boolean> {
|
|
171
|
+
if (this.filter && (await this.callHandle(this.filter, url, req, res))) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return await this.findRoute(url, req, res, true);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A persistent storage for the Api
|
|
3
|
+
*/
|
|
4
|
+
import { IAppSharedStorage } from '../models';
|
|
5
|
+
import { SimpleStorageDataProps } from '../models';
|
|
6
|
+
import { apiCache } from './api-cache';
|
|
7
|
+
|
|
8
|
+
// ApiSharedStorage is used in api module to store variables inside an api scope
|
|
9
|
+
export class ApiSharedStorage {
|
|
10
|
+
private storage: IAppSharedStorage | undefined;
|
|
11
|
+
|
|
12
|
+
public setAppSharedStorage(storage: IAppSharedStorage): void {
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
}
|
|
15
|
+
public getAppSharedStorage(): IAppSharedStorage {
|
|
16
|
+
if (!this.storage) {
|
|
17
|
+
throw new Error('AppSharedStorage not initialized');
|
|
18
|
+
}
|
|
19
|
+
return this.storage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// called from primary before exit, or from api to save changes
|
|
23
|
+
async save() {
|
|
24
|
+
const appName = apiCache.getAppName();
|
|
25
|
+
await this.getAppSharedStorage().save(appName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(key: string): Promise<string> {
|
|
29
|
+
const appName = apiCache.getAppName();
|
|
30
|
+
return this.getAppSharedStorage().get(appName, key);
|
|
31
|
+
}
|
|
32
|
+
getWeb(key: string): Promise<string> {
|
|
33
|
+
const appName = apiCache.getAppName();
|
|
34
|
+
return this.getAppSharedStorage().getWeb(appName, key);
|
|
35
|
+
}
|
|
36
|
+
getApi(key: string): Promise<string> {
|
|
37
|
+
const appName = apiCache.getAppName();
|
|
38
|
+
return this.getAppSharedStorage().getApi(appName, key);
|
|
39
|
+
}
|
|
40
|
+
getWebAll(): Promise<SimpleStorageDataProps> {
|
|
41
|
+
const appName = apiCache.getAppName();
|
|
42
|
+
return this.getAppSharedStorage().getWebAll(appName);
|
|
43
|
+
}
|
|
44
|
+
getWithPrefix(prefixKey: string): Promise<SimpleStorageDataProps> {
|
|
45
|
+
const appName = apiCache.getAppName();
|
|
46
|
+
return this.getAppSharedStorage().getWithPrefix(appName, prefixKey);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set(key: string, value: any) {
|
|
50
|
+
const appName = apiCache.getAppName();
|
|
51
|
+
return this.getAppSharedStorage().set(appName, key, value);
|
|
52
|
+
}
|
|
53
|
+
setWeb(key: string, value: any) {
|
|
54
|
+
const appName = apiCache.getAppName();
|
|
55
|
+
return this.getAppSharedStorage().setWeb(appName, key, value);
|
|
56
|
+
}
|
|
57
|
+
setApi(key: string, value: any) {
|
|
58
|
+
const appName = apiCache.getAppName();
|
|
59
|
+
return this.getAppSharedStorage().setApi(appName, key, value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// this can be used in app, but in api, it should use getAppStorage()
|
|
64
|
+
export const apiStorage = /* @__PURE__ */ new ApiSharedStorage();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import { MiniWebSocket } from './mini-web-socket';
|
|
4
|
+
import { ShellService } from './shell-service';
|
|
5
|
+
|
|
6
|
+
// This is only used in debug mode (no clusters)
|
|
7
|
+
export class DebugService {
|
|
8
|
+
static clientRefreshFlag = Date.now();
|
|
9
|
+
static miniWebSocket = new MiniWebSocket(this.onMessage.bind(this));
|
|
10
|
+
static shellMap = new Map<Duplex, ShellService>();
|
|
11
|
+
|
|
12
|
+
public static onMessage(msg: string, socket: Duplex) {
|
|
13
|
+
try {
|
|
14
|
+
const json = JSON.parse(msg);
|
|
15
|
+
if (json.message === 'get-flag') {
|
|
16
|
+
this.miniWebSocket.sendMessage(
|
|
17
|
+
socket,
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
message: 'flag',
|
|
20
|
+
flag: DebugService.clientRefreshFlag,
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
} else if (json.message === 'shell') {
|
|
24
|
+
console.log(json);
|
|
25
|
+
let shell: ShellService;
|
|
26
|
+
if (this.shellMap.has(socket)) {
|
|
27
|
+
shell = this.shellMap.get(socket)!;
|
|
28
|
+
} else {
|
|
29
|
+
shell = new ShellService(socket, this.miniWebSocket);
|
|
30
|
+
this.shellMap.set(socket, shell);
|
|
31
|
+
socket.on('close', () => {
|
|
32
|
+
shell.stop();
|
|
33
|
+
this.shellMap.delete(socket);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
shell.cmd(json.cmd);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
44
|
+
this.miniWebSocket.handleUpgrade(req, socket, head);
|
|
45
|
+
|
|
46
|
+
// socket.write(JSON.stringify({ message: 'flag', flag: DebugService.clientRefreshFlag }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// broadcast to all frontend clients
|
|
50
|
+
public static broadcastRefresh() {
|
|
51
|
+
console.log(`broadcast refresh request to clients.`);
|
|
52
|
+
this.clientRefreshFlag = Date.now();
|
|
53
|
+
const msg = { message: 'Refresh', flag: this.clientRefreshFlag };
|
|
54
|
+
this.miniWebSocket.broadcast(JSON.stringify(msg));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const encodeHtml = (str: string): string => {
|
|
2
|
+
return str.replace(
|
|
3
|
+
/[&<>'"]/g,
|
|
4
|
+
(tag) =>
|
|
5
|
+
({
|
|
6
|
+
'&': '&',
|
|
7
|
+
'<': '<',
|
|
8
|
+
'>': '>',
|
|
9
|
+
"'": ''',
|
|
10
|
+
'"': '"',
|
|
11
|
+
}[tag] || '')
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const decodeHtml = (str: string): string => {
|
|
16
|
+
return str.replace(
|
|
17
|
+
/&(\D+);/gi,
|
|
18
|
+
(tag) =>
|
|
19
|
+
({
|
|
20
|
+
'&': '&',
|
|
21
|
+
'<': '<',
|
|
22
|
+
'>': '>',
|
|
23
|
+
''': "'",
|
|
24
|
+
'"': '"',
|
|
25
|
+
}[tag] || '')
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { encodeHtml } from './encode-html';
|
|
3
|
+
import { IsType } from '../lib';
|
|
4
|
+
|
|
5
|
+
export const handler200 = (res: ServerResponse, msg: string | object, title = '', contentType?: string) => {
|
|
6
|
+
handlerResponse(res, 200, title, msg, contentType);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const handler304 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
10
|
+
handlerResponse(res, 304, '304 Not Modified', msg, contentType);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const handler403 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
14
|
+
handlerResponse(res, 403, '403 Forbidden', msg, contentType);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const handler405 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
18
|
+
handlerResponse(res, 405, '405 Method Not Allowed', msg, contentType);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const handler404 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
22
|
+
handlerResponse(res, 404, '404 Not Found', msg, contentType);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const handler416 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
26
|
+
handlerResponse(res, 416, '416 Range Not Satisfiable', msg, contentType);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const handler500 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
30
|
+
handlerResponse(res, 500, '500 Internal Server Error', msg, contentType);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const handler400 = (res: ServerResponse, msg?: string | object, contentType?: string) => {
|
|
34
|
+
handlerResponse(res, 400, '400 Bad Request', msg, contentType);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const handlerResponse = (
|
|
38
|
+
res: ServerResponse,
|
|
39
|
+
statusCode: number,
|
|
40
|
+
title: string,
|
|
41
|
+
msg?: string | object,
|
|
42
|
+
contentType?: string
|
|
43
|
+
) => {
|
|
44
|
+
res.statusCode = statusCode;
|
|
45
|
+
if (res.writable) {
|
|
46
|
+
try {
|
|
47
|
+
res.setHeader('content-type', contentType || 'text/html');
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// errors may have been triggered when headers being sent already
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const text = IsType.isObject(msg) ? JSON.stringify(msg) : (msg as string);
|
|
54
|
+
const html = contentType === 'application/json' ? msg : generateHtml(title, text || title);
|
|
55
|
+
res.end(html);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const generateHtml = (title: string, message: string) => {
|
|
59
|
+
return `${[
|
|
60
|
+
'<!doctype html>',
|
|
61
|
+
'<html>',
|
|
62
|
+
' <head>',
|
|
63
|
+
' <meta charset="utf-8">',
|
|
64
|
+
` <title>${title}</title>`,
|
|
65
|
+
' </head>',
|
|
66
|
+
' <body>',
|
|
67
|
+
` <p>${encodeHtml(message)}</p>`,
|
|
68
|
+
' </body>',
|
|
69
|
+
'</html>',
|
|
70
|
+
].join('\n')}\n`;
|
|
71
|
+
};
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './api-cache';
|
|
2
|
+
export * from './api-helper';
|
|
3
|
+
export * from './api-module';
|
|
4
|
+
export * from './api-router';
|
|
5
|
+
export * from './api-shared-storage';
|
|
6
|
+
export * from './async-storage';
|
|
7
|
+
export * from './debug-service';
|
|
8
|
+
export * from './encode-html';
|
|
9
|
+
export * from './handle-status';
|
|
10
|
+
export * from './mini-web-socket';
|
|
11
|
+
export * from './server-content-type';
|
|
12
|
+
export * from './server-render';
|
|
13
|
+
export * from './simple-storage';
|
|
14
|
+
export * from './static-server';
|
|
15
|
+
export * from './to-client-delivery';
|
|
16
|
+
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
// https://github.com/ErickWendel/websockets-with-nodejs-from-scratch/blob/main/nodejs-raw-websocket/server.mjs
|
|
6
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
|
7
|
+
// https://github.com/websockets/ws/blob/d343a0cf7bba29a4e14217cb010446bec8fdf444/lib/receiver.js
|
|
8
|
+
|
|
9
|
+
// This is a specification specific to WebSocket protocol
|
|
10
|
+
const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
11
|
+
|
|
12
|
+
const MASK_KEY_BYTES_LENGTH = 4;
|
|
13
|
+
// opcodes
|
|
14
|
+
const OPCODE_CONTINUATION = 0x0;
|
|
15
|
+
const OPCODE_TEXT = 0x1;
|
|
16
|
+
const OPCODE_BINARY = 0x2;
|
|
17
|
+
const OPCODE_CLOSE = 0x8;
|
|
18
|
+
const OPCODE_PING = 0x9;
|
|
19
|
+
const OPCODE_PONG = 0xa;
|
|
20
|
+
|
|
21
|
+
// This is only used in debug mode (no clusters)
|
|
22
|
+
export type MiniWebSocketMsgProps = (msg: string, socket: Duplex) => void;
|
|
23
|
+
export type MiniWebSocketCloseProps = (socket: Duplex, status: string) => void;
|
|
24
|
+
export class MiniWebSocket {
|
|
25
|
+
clientRefreshFlag = Date.now();
|
|
26
|
+
clients = new Set<Duplex>();
|
|
27
|
+
private _onMessage: MiniWebSocketMsgProps;
|
|
28
|
+
private _onClose?: MiniWebSocketCloseProps;
|
|
29
|
+
|
|
30
|
+
constructor(onMessage: MiniWebSocketMsgProps, onClose?: MiniWebSocketCloseProps) {
|
|
31
|
+
this._onMessage = onMessage;
|
|
32
|
+
this._onClose = onClose;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer) {
|
|
36
|
+
const key = req.headers['sec-websocket-key'];
|
|
37
|
+
const upgrade = (req.headers.upgrade || '').toString().toLowerCase();
|
|
38
|
+
const connection = (req.headers.connection || '').toString().toLowerCase();
|
|
39
|
+
|
|
40
|
+
if (!key || upgrade !== 'websocket' || !connection.includes('upgrade')) {
|
|
41
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
42
|
+
socket.destroy();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cleanup = (socket: Duplex, status: string) => {
|
|
47
|
+
if (this.clients.has(socket)) this.clients.delete(socket);
|
|
48
|
+
if (!socket.destroyed) socket.destroy();
|
|
49
|
+
if (this._onClose) this._onClose(socket, status);
|
|
50
|
+
};
|
|
51
|
+
this.clients.add(socket);
|
|
52
|
+
socket.on('close', () => cleanup(socket, 'close'));
|
|
53
|
+
socket.on('error', () => cleanup(socket, 'error'));
|
|
54
|
+
socket.on('end', () => cleanup(socket, 'end'));
|
|
55
|
+
socket.write(this.handleHeaders(key));
|
|
56
|
+
|
|
57
|
+
// buffer for cumulative data, parse frame; first append head (possible residual data)
|
|
58
|
+
let buffer = head && head.length ? Buffer.from(head) : Buffer.alloc(0);
|
|
59
|
+
const onData = (chunk: Buffer) => {
|
|
60
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
61
|
+
// try to parse as many complete frames as possible
|
|
62
|
+
parseFrames();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const parseFrames = () => {
|
|
66
|
+
while (true) {
|
|
67
|
+
if (buffer.length < 2) return;
|
|
68
|
+
|
|
69
|
+
const firstByte = buffer[0];
|
|
70
|
+
const secondByte = buffer[1];
|
|
71
|
+
|
|
72
|
+
const fin = (firstByte & 0x80) !== 0; // FIN bit
|
|
73
|
+
const rsv = firstByte & 0x70; // must be 0
|
|
74
|
+
const opcode = firstByte & 0x0f;
|
|
75
|
+
|
|
76
|
+
const masked = (secondByte & 0x80) !== 0; // must be 1
|
|
77
|
+
let payloadLen = secondByte & 0x7f;
|
|
78
|
+
|
|
79
|
+
let offset = 2;
|
|
80
|
+
|
|
81
|
+
// illegal case: RSV set bits; or client not mask
|
|
82
|
+
if (rsv !== 0 || !masked) {
|
|
83
|
+
this.sendClose(socket, 1002); // protocol error
|
|
84
|
+
cleanup(socket, 'protocol error');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// only accept single frame (FIN=1) simple message, ignore fragmented/continuation frame
|
|
89
|
+
if (!fin && opcode !== OPCODE_CONTINUATION) {
|
|
90
|
+
this.sendClose(socket, 1003); // unsupported data
|
|
91
|
+
cleanup(socket, 'unsupported opcode');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (payloadLen === 126) {
|
|
96
|
+
if (buffer.length < offset + 2) return;
|
|
97
|
+
payloadLen = buffer.readUInt16BE(offset);
|
|
98
|
+
offset += 2;
|
|
99
|
+
} else if (payloadLen === 127) {
|
|
100
|
+
// read 64-bit length: we only take low 32 bits (enough for your scene)
|
|
101
|
+
if (buffer.length < offset + 8) return;
|
|
102
|
+
// ignore high 32
|
|
103
|
+
payloadLen = buffer.readUInt32BE(offset + 4);
|
|
104
|
+
offset += 8;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const needed = offset + MASK_KEY_BYTES_LENGTH + payloadLen;
|
|
108
|
+
if (buffer.length < needed) return;
|
|
109
|
+
|
|
110
|
+
const maskKey = buffer.slice(offset, offset + MASK_KEY_BYTES_LENGTH);
|
|
111
|
+
offset += MASK_KEY_BYTES_LENGTH;
|
|
112
|
+
|
|
113
|
+
let payload = buffer.slice(offset, offset + payloadLen);
|
|
114
|
+
// unmask
|
|
115
|
+
for (let i = 0; i < payload.length; i++) {
|
|
116
|
+
payload[i] ^= maskKey[i % MASK_KEY_BYTES_LENGTH];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// consume this frame
|
|
120
|
+
buffer = buffer.slice(needed);
|
|
121
|
+
|
|
122
|
+
switch (opcode) {
|
|
123
|
+
case OPCODE_TEXT: {
|
|
124
|
+
const text = payload.toString('utf8');
|
|
125
|
+
try {
|
|
126
|
+
this._onMessage(text, socket);
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore user callback error
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case OPCODE_BINARY: {
|
|
133
|
+
// don't support binary, close
|
|
134
|
+
this.sendClose(socket, 1003); // unsupported data
|
|
135
|
+
cleanup(socket, 'unsupported binary');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case OPCODE_PING: {
|
|
139
|
+
// reply Pong, with same payload
|
|
140
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_PONG));
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case OPCODE_PONG: {
|
|
144
|
+
// ignore
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case OPCODE_CLOSE: {
|
|
148
|
+
// parse status code
|
|
149
|
+
let code = 1000;
|
|
150
|
+
if (payload.length >= 2) {
|
|
151
|
+
code = payload.readUInt16BE(0);
|
|
152
|
+
}
|
|
153
|
+
// reply Close, then end
|
|
154
|
+
this.sendClose(socket, code);
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
try {
|
|
157
|
+
socket.end();
|
|
158
|
+
} catch {}
|
|
159
|
+
cleanup(socket, 'close');
|
|
160
|
+
}, 20);
|
|
161
|
+
return; // end loop
|
|
162
|
+
}
|
|
163
|
+
case OPCODE_CONTINUATION: {
|
|
164
|
+
// don't support continuation, close
|
|
165
|
+
this.sendClose(socket, 1003);
|
|
166
|
+
cleanup(socket, 'unsupported continuation');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
default: {
|
|
170
|
+
// unknown opcode
|
|
171
|
+
this.sendClose(socket, 1002);
|
|
172
|
+
cleanup(socket, 'unknown opcode');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
socket.on('data', onData);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private handleHeaders(key: string) {
|
|
182
|
+
const acceptKey = crypto
|
|
183
|
+
.createHash('sha1')
|
|
184
|
+
.update(key + WEBSOCKET_GUID)
|
|
185
|
+
.digest('base64');
|
|
186
|
+
const headers = [
|
|
187
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
188
|
+
// 'Content-Type: text/html',
|
|
189
|
+
'Upgrade: websocket',
|
|
190
|
+
'Connection: Upgrade',
|
|
191
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
192
|
+
'\r\n',
|
|
193
|
+
];
|
|
194
|
+
return headers.join('\r\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public broadcast(msg: string) {
|
|
198
|
+
const payload = Buffer.from(msg);
|
|
199
|
+
const frame = this.encodeFrame(payload, OPCODE_TEXT);
|
|
200
|
+
for (const socket of this.clients) {
|
|
201
|
+
this.safeWrite(socket, frame);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
public sendMessage(socket: Duplex, msg: string) {
|
|
205
|
+
const payload = Buffer.from(msg);
|
|
206
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_TEXT));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
encodeFrame(payload: Buffer, opcode: number) {
|
|
210
|
+
const len = payload.length;
|
|
211
|
+
|
|
212
|
+
// 0x80 === 128 in binary
|
|
213
|
+
// FIN=1 + opcode
|
|
214
|
+
if (len <= 125) {
|
|
215
|
+
const header = Buffer.from([0x80 | opcode, len]);
|
|
216
|
+
return Buffer.concat([header, payload]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (len < 0x10000) {
|
|
220
|
+
// alloc 4 bytes
|
|
221
|
+
// [0] - 128 + 1 - 10000001 fin + opcode
|
|
222
|
+
// [1] - 126 + 0 - payload length marker + mask indicator
|
|
223
|
+
// [2] 0 - content length
|
|
224
|
+
// [3] 113 - content length
|
|
225
|
+
// [ 4 - ..] - the message itself
|
|
226
|
+
const header = Buffer.allocUnsafe(4);
|
|
227
|
+
header[0] = 0x80 | opcode;
|
|
228
|
+
header[1] = 126;
|
|
229
|
+
header.writeUInt16BE(len, 2);
|
|
230
|
+
return Buffer.concat([header, payload]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 64-bit length:write high 32 bits as 0, low 32 bits as len (enough for your scene)
|
|
234
|
+
const header = Buffer.allocUnsafe(10);
|
|
235
|
+
header[0] = 0x80 | opcode;
|
|
236
|
+
header[1] = 127;
|
|
237
|
+
header.writeUInt32BE(0, 2); // high 32
|
|
238
|
+
header.writeUInt32BE(len, 6); // low 32
|
|
239
|
+
return Buffer.concat([header, payload]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
sendClose(socket: Duplex, code = 1000) {
|
|
243
|
+
const payload = Buffer.allocUnsafe(2);
|
|
244
|
+
payload.writeUInt16BE(code, 0);
|
|
245
|
+
this.safeWrite(socket, this.encodeFrame(payload, OPCODE_CLOSE));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
safeWrite(socket: Duplex, data: Buffer) {
|
|
249
|
+
try {
|
|
250
|
+
if (!socket.destroyed) socket.write(data);
|
|
251
|
+
} catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
close(socket: Duplex, code = 1000) {
|
|
257
|
+
this.sendClose(socket, code);
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
try {
|
|
260
|
+
socket.end();
|
|
261
|
+
} catch {}
|
|
262
|
+
}, 20);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
closeAll(code = 1000) {
|
|
266
|
+
for (const s of this.clients) {
|
|
267
|
+
this.close(s, code);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|