@kevisual/router 0.0.10 → 0.0.11
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/dist/router-browser.d.ts +16 -1
- package/dist/router-browser.js +9 -1
- package/dist/router-simple.js +1 -1
- package/dist/router.d.ts +21 -6
- package/dist/router.js +17 -6
- package/package.json +11 -9
- package/src/app.ts +103 -0
- package/src/browser.ts +9 -0
- package/src/connect.ts +67 -0
- package/src/index.ts +16 -0
- package/src/io.ts +6 -0
- package/src/mod.ts +13 -0
- package/src/result/error.ts +67 -0
- package/src/result/index.ts +1 -0
- package/src/route.ts +714 -0
- package/src/router-simple-lib.ts +3 -0
- package/src/router-simple.ts +76 -0
- package/src/server/deno-ws-server.ts +0 -0
- package/src/server/handle-server.ts +57 -0
- package/src/server/index.ts +2 -0
- package/src/server/parse-body.ts +24 -0
- package/src/server/parse-xml.ts +31 -0
- package/src/server/server.ts +233 -0
- package/src/server/ws-server.ts +160 -0
- package/src/sign.ts +46 -0
- package/src/static.ts +97 -0
- package/src/utils/parse.ts +13 -0
- package/src/utils/pick.ts +9 -0
- package/src/utils/route-map.ts +47 -0
- package/src/validator/index.ts +1 -0
- package/src/validator/rule.ts +95 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { pathToRegexp, Key } from 'path-to-regexp';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
+
import { parseBody, parseSearch } from './server/parse-body.ts';
|
|
4
|
+
|
|
5
|
+
type Req = IncomingMessage & { params?: Record<string, string> };
|
|
6
|
+
interface Route {
|
|
7
|
+
method: string;
|
|
8
|
+
regexp: RegExp;
|
|
9
|
+
keys: Key[];
|
|
10
|
+
handlers: Array<(req: Req, res: ServerResponse) => Promise<void> | void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* SimpleRouter
|
|
14
|
+
*/
|
|
15
|
+
export class SimpleRouter {
|
|
16
|
+
routes: Route[] = [];
|
|
17
|
+
exclude: string[] = []; // 排除的请求
|
|
18
|
+
constructor(opts?: { exclude?: string[] }) {
|
|
19
|
+
this.exclude = opts?.exclude || ['/api/router'];
|
|
20
|
+
}
|
|
21
|
+
getBody(req: Req) {
|
|
22
|
+
return parseBody<Record<string, any>>(req);
|
|
23
|
+
}
|
|
24
|
+
getSearch(req: Req) {
|
|
25
|
+
return parseSearch(req);
|
|
26
|
+
}
|
|
27
|
+
use(method: string, route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
28
|
+
const handlers = Array.isArray(fns) ? fns.flat() : [];
|
|
29
|
+
const pattern = pathToRegexp(route);
|
|
30
|
+
|
|
31
|
+
this.routes.push({ method: method.toLowerCase(), regexp: pattern.regexp, keys: pattern.keys, handlers });
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
get(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
35
|
+
return this.use('get', route, ...fns);
|
|
36
|
+
}
|
|
37
|
+
post(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
38
|
+
return this.use('post', route, ...fns);
|
|
39
|
+
}
|
|
40
|
+
all(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise<void> | void>) {
|
|
41
|
+
this.use('post', route, ...fns);
|
|
42
|
+
this.use('get', route, ...fns);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 解析 req 和 res 请求
|
|
47
|
+
* @param req
|
|
48
|
+
* @param res
|
|
49
|
+
* @returns
|
|
50
|
+
*/
|
|
51
|
+
parse(req: Req, res: ServerResponse) {
|
|
52
|
+
const { pathname } = new URL(req.url, 'http://localhost');
|
|
53
|
+
const method = req.method.toLowerCase();
|
|
54
|
+
if (this.exclude.includes(pathname)) {
|
|
55
|
+
return 'is_exclude';
|
|
56
|
+
}
|
|
57
|
+
const route = this.routes.find((route) => {
|
|
58
|
+
const matchResult = route.regexp.exec(pathname);
|
|
59
|
+
if (matchResult && route.method === method) {
|
|
60
|
+
const params: Record<string, string> = {};
|
|
61
|
+
route.keys.forEach((key, i) => {
|
|
62
|
+
params[key.name] = matchResult[i + 1];
|
|
63
|
+
});
|
|
64
|
+
req.params = params;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (route) {
|
|
70
|
+
const { handlers } = route;
|
|
71
|
+
return handlers.reduce((promiseChain, handler) => promiseChain.then(() => Promise.resolve(handler(req, res))), Promise.resolve());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 'not_found';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { parseBody } from './parse-body.ts';
|
|
3
|
+
import url from 'node:url';
|
|
4
|
+
import { createHandleCtx } from './server.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* get params and body
|
|
8
|
+
* 优先原则
|
|
9
|
+
* 1. 请求参数中的 payload 的token 优先
|
|
10
|
+
* 2. 请求头中的 authorization 优先
|
|
11
|
+
* 3. 请求头中的 cookie 优先
|
|
12
|
+
* @param req
|
|
13
|
+
* @param res
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
export const handleServer = async (req: IncomingMessage, res: ServerResponse) => {
|
|
17
|
+
if (req.url === '/favicon.ico') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const can = ['get', 'post'];
|
|
21
|
+
const method = req.method.toLocaleLowerCase();
|
|
22
|
+
if (!can.includes(method)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const parsedUrl = url.parse(req.url, true);
|
|
26
|
+
// 获取token
|
|
27
|
+
let token = req.headers['authorization'] || '';
|
|
28
|
+
const handle = createHandleCtx(req, res);
|
|
29
|
+
const cookies = handle.req.cookies;
|
|
30
|
+
if (!token) {
|
|
31
|
+
token = cookies.token; // cookie优先
|
|
32
|
+
}
|
|
33
|
+
if (token) {
|
|
34
|
+
token = token.replace('Bearer ', '');
|
|
35
|
+
}
|
|
36
|
+
// 获取查询参数
|
|
37
|
+
const param = parsedUrl.query;
|
|
38
|
+
let body: Record<any, any>;
|
|
39
|
+
if (method === 'post') {
|
|
40
|
+
body = await parseBody(req);
|
|
41
|
+
}
|
|
42
|
+
if (param?.payload && typeof param.payload === 'string') {
|
|
43
|
+
try {
|
|
44
|
+
const payload = JSON.parse(param.payload as string);
|
|
45
|
+
param.payload = payload;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error(e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const data = {
|
|
51
|
+
token,
|
|
52
|
+
...param,
|
|
53
|
+
...body,
|
|
54
|
+
cookies,
|
|
55
|
+
};
|
|
56
|
+
return data;
|
|
57
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import url from 'node:url';
|
|
3
|
+
|
|
4
|
+
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
|
5
|
+
return new Promise<T>((resolve, reject) => {
|
|
6
|
+
const arr: any[] = [];
|
|
7
|
+
req.on('data', (chunk) => {
|
|
8
|
+
arr.push(chunk);
|
|
9
|
+
});
|
|
10
|
+
req.on('end', () => {
|
|
11
|
+
try {
|
|
12
|
+
const body = Buffer.concat(arr).toString();
|
|
13
|
+
resolve(JSON.parse(body));
|
|
14
|
+
} catch (e) {
|
|
15
|
+
resolve({} as T);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const parseSearch = (req: IncomingMessage) => {
|
|
22
|
+
const parsedUrl = url.parse(req.url, true);
|
|
23
|
+
return parsedUrl.query;
|
|
24
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import xml2js from 'xml2js';
|
|
2
|
+
|
|
3
|
+
export const parseXml = async (req: any): Promise<any> => {
|
|
4
|
+
return await new Promise((resolve) => {
|
|
5
|
+
// 读取请求数据
|
|
6
|
+
let data = '';
|
|
7
|
+
req.setEncoding('utf8');
|
|
8
|
+
// 监听data事件,接收数据片段
|
|
9
|
+
req.on('data', (chunk) => {
|
|
10
|
+
data += chunk;
|
|
11
|
+
});
|
|
12
|
+
// 当请求结束时处理数据
|
|
13
|
+
req.on('end', () => {
|
|
14
|
+
try {
|
|
15
|
+
// 使用xml2js解析XML
|
|
16
|
+
xml2js.parseString(data, function (err, result) {
|
|
17
|
+
if (err) {
|
|
18
|
+
console.error('XML解析错误:', err);
|
|
19
|
+
resolve(null);
|
|
20
|
+
} else {
|
|
21
|
+
const jsonString = JSON.stringify(result);
|
|
22
|
+
resolve(jsonString);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('处理请求时出错:', error);
|
|
27
|
+
resolve(null);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import http2 from 'node:http2';
|
|
5
|
+
import { handleServer } from './handle-server.ts';
|
|
6
|
+
import * as cookie from 'cookie';
|
|
7
|
+
export type Listener = (...args: any[]) => void;
|
|
8
|
+
|
|
9
|
+
type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void;
|
|
10
|
+
|
|
11
|
+
export type HandleCtx = {
|
|
12
|
+
req: IncomingMessage & { cookies: Record<string, string> };
|
|
13
|
+
res: ServerResponse & {
|
|
14
|
+
/**
|
|
15
|
+
* cookie 函数, end 参数用于设置是否立即设置到响应头,设置了后面的cookie再设置会覆盖前面的
|
|
16
|
+
*/
|
|
17
|
+
cookie: CookieFn; //
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
// 实现函数
|
|
21
|
+
export function createHandleCtx(req: IncomingMessage, res: ServerResponse): HandleCtx {
|
|
22
|
+
// 用于存储所有的 Set-Cookie 字符串
|
|
23
|
+
const cookies: string[] = [];
|
|
24
|
+
let handReq = req as HandleCtx['req'];
|
|
25
|
+
let handRes = res as HandleCtx['res'];
|
|
26
|
+
// 扩展 res.cookie 方法
|
|
27
|
+
const cookieFn: CookieFn = (name, value, options = {}, end = true) => {
|
|
28
|
+
// 序列化新的 Cookie
|
|
29
|
+
const serializedCookie = cookie.serialize(name, value, options);
|
|
30
|
+
cookies.push(serializedCookie); // 将新的 Cookie 添加到数组
|
|
31
|
+
if (end) {
|
|
32
|
+
// 如果设置了 end 参数,则立即设置到响应头
|
|
33
|
+
res.setHeader('Set-Cookie', cookies);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
// 解析请求中的现有 Cookie
|
|
37
|
+
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
|
38
|
+
handReq.cookies = parsedCookies;
|
|
39
|
+
handRes.cookie = cookieFn;
|
|
40
|
+
// 返回扩展的上下文
|
|
41
|
+
return {
|
|
42
|
+
req: handReq,
|
|
43
|
+
res: handRes,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export type Cors = {
|
|
47
|
+
/**
|
|
48
|
+
* @default '*''
|
|
49
|
+
*/
|
|
50
|
+
origin?: string | undefined;
|
|
51
|
+
};
|
|
52
|
+
export type ServerOpts = {
|
|
53
|
+
/**path default `/api/router` */
|
|
54
|
+
path?: string;
|
|
55
|
+
/**handle Fn */
|
|
56
|
+
handle?: (msg?: { path: string; key?: string; [key: string]: any }, ctx?: { req: http.IncomingMessage; res: http.ServerResponse }) => any;
|
|
57
|
+
cors?: Cors;
|
|
58
|
+
httpType?: 'http' | 'https' | 'http2';
|
|
59
|
+
httpsKey?: string;
|
|
60
|
+
httpsCert?: string;
|
|
61
|
+
};
|
|
62
|
+
export const resultError = (error: string, code = 500) => {
|
|
63
|
+
const r = {
|
|
64
|
+
code: code,
|
|
65
|
+
message: error,
|
|
66
|
+
};
|
|
67
|
+
return JSON.stringify(r);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class Server {
|
|
71
|
+
path = '/api/router';
|
|
72
|
+
private _server: http.Server | https.Server | http2.Http2SecureServer;
|
|
73
|
+
public handle: ServerOpts['handle'];
|
|
74
|
+
private _callback: any;
|
|
75
|
+
private cors: Cors;
|
|
76
|
+
private hasOn = false;
|
|
77
|
+
private httpType = 'http';
|
|
78
|
+
private options = {
|
|
79
|
+
key: '',
|
|
80
|
+
cert: '',
|
|
81
|
+
};
|
|
82
|
+
constructor(opts?: ServerOpts) {
|
|
83
|
+
this.path = opts?.path || '/api/router';
|
|
84
|
+
this.handle = opts?.handle;
|
|
85
|
+
this.cors = opts?.cors;
|
|
86
|
+
this.httpType = opts?.httpType || 'http';
|
|
87
|
+
this.options = {
|
|
88
|
+
key: opts?.httpsKey || '',
|
|
89
|
+
cert: opts?.httpsCert || '',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
|
93
|
+
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
|
94
|
+
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
|
95
|
+
listen(port: number, listeningListener?: () => void): void;
|
|
96
|
+
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
|
97
|
+
listen(path: string, listeningListener?: () => void): void;
|
|
98
|
+
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
|
99
|
+
listen(handle: any, listeningListener?: () => void): void;
|
|
100
|
+
listen(...args: any[]) {
|
|
101
|
+
this._server = this.createServer();
|
|
102
|
+
const callback = this.createCallback();
|
|
103
|
+
this._server.on('request', callback);
|
|
104
|
+
this._server.listen(...args);
|
|
105
|
+
}
|
|
106
|
+
createServer() {
|
|
107
|
+
let server: http.Server | https.Server | http2.Http2SecureServer;
|
|
108
|
+
const httpType = this.httpType;
|
|
109
|
+
if (httpType === 'https') {
|
|
110
|
+
if (this.options.key && this.options.cert) {
|
|
111
|
+
server = https.createServer({
|
|
112
|
+
key: this.options.key,
|
|
113
|
+
cert: this.options.cert,
|
|
114
|
+
});
|
|
115
|
+
return server;
|
|
116
|
+
} else {
|
|
117
|
+
console.error('https key and cert is required');
|
|
118
|
+
console.log('downgrade to http');
|
|
119
|
+
}
|
|
120
|
+
} else if (httpType === 'http2') {
|
|
121
|
+
if (this.options.key && this.options.cert) {
|
|
122
|
+
server = http2.createSecureServer({
|
|
123
|
+
key: this.options.key,
|
|
124
|
+
cert: this.options.cert,
|
|
125
|
+
});
|
|
126
|
+
return server;
|
|
127
|
+
} else {
|
|
128
|
+
console.error('https key and cert is required');
|
|
129
|
+
console.log('downgrade to http');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
server = http.createServer();
|
|
133
|
+
return server;
|
|
134
|
+
}
|
|
135
|
+
setHandle(handle?: any) {
|
|
136
|
+
this.handle = handle;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* get callback
|
|
140
|
+
* @returns
|
|
141
|
+
*/
|
|
142
|
+
createCallback() {
|
|
143
|
+
const path = this.path;
|
|
144
|
+
const handle = this.handle;
|
|
145
|
+
const cors = this.cors;
|
|
146
|
+
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
|
147
|
+
// only handle /api/router
|
|
148
|
+
if (req.url === '/favicon.ico') {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (res.headersSent) {
|
|
152
|
+
// 程序已经在其他地方响应了
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (this.hasOn && !req.url.startsWith(path)) {
|
|
156
|
+
// 其他监听存在,不判断不是当前路径的请求,
|
|
157
|
+
// 也就是不处理!url.startsWith(path)这个请求了
|
|
158
|
+
// 交给其他监听处理
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (cors) {
|
|
162
|
+
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
|
|
163
|
+
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
164
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
|
165
|
+
if (req.method === 'OPTIONS') {
|
|
166
|
+
res.end();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const url = req.url;
|
|
171
|
+
if (!url.startsWith(path)) {
|
|
172
|
+
res.end(resultError(`not path:[${path}]`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const messages = await handleServer(req, res);
|
|
176
|
+
if (!handle) {
|
|
177
|
+
res.end(resultError('no handle'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const end = await handle(messages as any, { req, res });
|
|
182
|
+
if (res.writableEnded) {
|
|
183
|
+
// 如果响应已经结束,则不进行任何操作
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
187
|
+
if (typeof end === 'string') {
|
|
188
|
+
res.end(end);
|
|
189
|
+
} else {
|
|
190
|
+
res.end(JSON.stringify(end));
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error(e);
|
|
194
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
195
|
+
if (e.code && typeof e.code === 'number') {
|
|
196
|
+
res.end(resultError(e.message || `Router Server error`, e.code));
|
|
197
|
+
} else {
|
|
198
|
+
res.end(resultError('Router Server error'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
this._callback = _callback;
|
|
203
|
+
return _callback;
|
|
204
|
+
}
|
|
205
|
+
get handleServer() {
|
|
206
|
+
return this._callback;
|
|
207
|
+
}
|
|
208
|
+
set handleServer(fn: any) {
|
|
209
|
+
this._callback = fn;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 兜底监听,当除开 `/api/router` 之外的请求,框架只监听一个api,所以有其他的请求都执行其他的监听
|
|
213
|
+
* @description 主要是为了兼容其他的监听
|
|
214
|
+
* @param listener
|
|
215
|
+
*/
|
|
216
|
+
on(listener: Listener | Listener[]) {
|
|
217
|
+
this._server = this._server || this.createServer();
|
|
218
|
+
this._server.removeAllListeners('request');
|
|
219
|
+
this.hasOn = true;
|
|
220
|
+
if (Array.isArray(listener)) {
|
|
221
|
+
listener.forEach((l) => this._server.on('request', l));
|
|
222
|
+
} else {
|
|
223
|
+
this._server.on('request', listener);
|
|
224
|
+
}
|
|
225
|
+
this._server.on('request', this._callback || this.createCallback());
|
|
226
|
+
}
|
|
227
|
+
get callback() {
|
|
228
|
+
return this._callback || this.createCallback();
|
|
229
|
+
}
|
|
230
|
+
get server() {
|
|
231
|
+
return this._server;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import type { WebSocket } from 'ws';
|
|
3
|
+
import { Server } from './server.ts';
|
|
4
|
+
import { parseIfJson } from '../utils/parse.ts';
|
|
5
|
+
|
|
6
|
+
export const createWsServer = (server: Server) => {
|
|
7
|
+
// 将 WebSocket 服务器附加到 HTTP 服务器
|
|
8
|
+
const wss = new WebSocketServer({ server: server.server as any });
|
|
9
|
+
return wss;
|
|
10
|
+
};
|
|
11
|
+
type WsServerBaseOpts = {
|
|
12
|
+
wss?: WebSocketServer;
|
|
13
|
+
path?: string;
|
|
14
|
+
};
|
|
15
|
+
export type ListenerFn = (message: { data: Record<string, any>; ws: WebSocket; end: (data: any) => any }) => Promise<any>;
|
|
16
|
+
export type Listener<T = 'router' | 'chat' | 'ai'> = {
|
|
17
|
+
type: T;
|
|
18
|
+
listener: ListenerFn;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class WsServerBase {
|
|
22
|
+
wss: WebSocketServer;
|
|
23
|
+
path: string;
|
|
24
|
+
listeners: { type: string; listener: ListenerFn }[] = [];
|
|
25
|
+
listening: boolean = false;
|
|
26
|
+
constructor(opts: WsServerBaseOpts) {
|
|
27
|
+
this.wss = opts.wss;
|
|
28
|
+
if (!this.wss) {
|
|
29
|
+
throw new Error('wss is required');
|
|
30
|
+
}
|
|
31
|
+
this.path = opts.path || '';
|
|
32
|
+
}
|
|
33
|
+
setPath(path: string) {
|
|
34
|
+
this.path = path;
|
|
35
|
+
}
|
|
36
|
+
listen() {
|
|
37
|
+
if (this.listening) {
|
|
38
|
+
console.error('WsServer is listening');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.listening = true;
|
|
42
|
+
|
|
43
|
+
this.wss.on('connection', (ws) => {
|
|
44
|
+
ws.on('message', async (message: string) => {
|
|
45
|
+
const data = parseIfJson(message);
|
|
46
|
+
if (typeof data === 'string') {
|
|
47
|
+
ws.emit('string', data);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { type, data: typeData, ...rest } = data;
|
|
51
|
+
if (!type) {
|
|
52
|
+
ws.send(JSON.stringify({ code: 500, message: 'type is required' }));
|
|
53
|
+
}
|
|
54
|
+
const listeners = this.listeners.find((item) => item.type === type);
|
|
55
|
+
const res = {
|
|
56
|
+
type,
|
|
57
|
+
data: {} as any,
|
|
58
|
+
...rest,
|
|
59
|
+
};
|
|
60
|
+
const end = (data: any, all?: Record<string, any>) => {
|
|
61
|
+
const result = {
|
|
62
|
+
...res,
|
|
63
|
+
data,
|
|
64
|
+
...all,
|
|
65
|
+
};
|
|
66
|
+
ws.send(JSON.stringify(result));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!listeners) {
|
|
70
|
+
const data = { code: 500, message: `${type} server is error` };
|
|
71
|
+
end(data);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
listeners.listener({
|
|
75
|
+
data: typeData,
|
|
76
|
+
ws,
|
|
77
|
+
end: end,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
ws.on('string', (message: string) => {
|
|
81
|
+
if (message === 'close') {
|
|
82
|
+
ws.close();
|
|
83
|
+
}
|
|
84
|
+
if (message === 'ping') {
|
|
85
|
+
ws.send('pong');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
ws.send('connected');
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
addListener(type: string, listener: ListenerFn) {
|
|
92
|
+
if (!type || !listener) {
|
|
93
|
+
throw new Error('type and listener is required');
|
|
94
|
+
}
|
|
95
|
+
const find = this.listeners.find((item) => item.type === type);
|
|
96
|
+
if (find) {
|
|
97
|
+
this.listeners = this.listeners.filter((item) => item.type !== type);
|
|
98
|
+
}
|
|
99
|
+
this.listeners.push({ type, listener });
|
|
100
|
+
}
|
|
101
|
+
removeListener(type: string) {
|
|
102
|
+
this.listeners = this.listeners.filter((item) => item.type !== type);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// TODO: ws handle and path and routerContext
|
|
106
|
+
export class WsServer extends WsServerBase {
|
|
107
|
+
server: Server;
|
|
108
|
+
constructor(server: Server, opts?: any) {
|
|
109
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
110
|
+
const path = server.path;
|
|
111
|
+
super({ wss });
|
|
112
|
+
this.server = server;
|
|
113
|
+
this.setPath(opts?.path || path);
|
|
114
|
+
this.initListener();
|
|
115
|
+
}
|
|
116
|
+
initListener() {
|
|
117
|
+
const server = this.server;
|
|
118
|
+
const listener: Listener = {
|
|
119
|
+
type: 'router',
|
|
120
|
+
listener: async ({ data, ws, end }) => {
|
|
121
|
+
if (!server) {
|
|
122
|
+
end({ code: 500, message: 'server handle is error' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const handle = this.server.handle;
|
|
126
|
+
try {
|
|
127
|
+
const result = await handle(data as any);
|
|
128
|
+
end(result);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e.code && typeof e.code === 'number') {
|
|
131
|
+
end({
|
|
132
|
+
code: e.code,
|
|
133
|
+
message: e.message,
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
end({ code: 500, message: 'Router Server error' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
this.addListener(listener.type, listener.listener);
|
|
142
|
+
}
|
|
143
|
+
listen() {
|
|
144
|
+
super.listen();
|
|
145
|
+
const server = this.server;
|
|
146
|
+
const wss = this.wss;
|
|
147
|
+
// HTTP 服务器的 upgrade 事件
|
|
148
|
+
server.server.on('upgrade', (req, socket, head) => {
|
|
149
|
+
if (req.url === this.path) {
|
|
150
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
151
|
+
// 这里手动触发 connection 事件
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
wss.emit('connection', ws, req);
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
socket.destroy();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/sign.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { generate } from 'selfsigned';
|
|
2
|
+
|
|
3
|
+
type Attributes = {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
type AltNames = {
|
|
8
|
+
type: number;
|
|
9
|
+
value?: string;
|
|
10
|
+
ip?: string;
|
|
11
|
+
};
|
|
12
|
+
export const createCert = (attrs: Attributes[] = [], altNames: AltNames[] = []) => {
|
|
13
|
+
let attributes = [
|
|
14
|
+
{ name: 'commonName', value: '*' }, // 通配符域名
|
|
15
|
+
{ name: 'countryName', value: 'CN' }, // 国家代码
|
|
16
|
+
{ name: 'stateOrProvinceName', value: 'ZheJiang' }, // 州名
|
|
17
|
+
{ name: 'localityName', value: 'Hangzhou' }, // 城市名
|
|
18
|
+
{ name: 'organizationName', value: 'Envision' }, // 组织名
|
|
19
|
+
{ name: 'organizationalUnitName', value: 'IT' }, // 组织单位
|
|
20
|
+
...attrs,
|
|
21
|
+
];
|
|
22
|
+
// attribute 根据name去重复, 后面的覆盖前面的
|
|
23
|
+
attributes = attributes.filter((item, index, self) => {
|
|
24
|
+
return self.findIndex((t) => t.name === item.name) === index;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const options = {
|
|
28
|
+
days: 365, // 证书有效期(天)
|
|
29
|
+
extensions: [
|
|
30
|
+
{
|
|
31
|
+
name: 'subjectAltName',
|
|
32
|
+
altNames: [
|
|
33
|
+
{ type: 2, value: '*' }, // DNS 名称
|
|
34
|
+
{ type: 2, value: 'localhost' }, // DNS
|
|
35
|
+
{ type: 7, ip: '127.0.0.1' }, // IP 地址
|
|
36
|
+
...altNames,
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
const pems = generate(attributes, options);
|
|
42
|
+
return {
|
|
43
|
+
key: pems.private,
|
|
44
|
+
cert: pems.cert,
|
|
45
|
+
};
|
|
46
|
+
};
|