oox 0.3.3 → 0.3.5

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.
@@ -1,10 +1,11 @@
1
1
  import * as http from 'node:http';
2
2
  import * as https from 'node:https';
3
- import { httpRequest, parseHTTPBody } from './utils.js';
3
+ import { parseHTTPBody } from './utils.js';
4
4
  import Router from './router.js';
5
5
  import * as oox from '../../index.js';
6
- import Module, { ModuleConfig } from '../module.js';
7
- export class HTTPConfig extends ModuleConfig {
6
+ import Module from '../module.js';
7
+ export class HTTPConfig {
8
+ enabled = true;
8
9
  // listen port
9
10
  port = 0;
10
11
  // service path
@@ -80,9 +81,9 @@ export default class HTTPModule extends Module {
80
81
  if (ssl.enabled) {
81
82
  const fs = await import('node:fs');
82
83
  const options = {
83
- key: ssl.key ? fs.readFileSync(ssl.key) : null,
84
- cert: ssl.cert ? fs.readFileSync(ssl.cert) : null,
85
- ca: ssl.ca ? fs.readFileSync(ssl.ca) : null
84
+ key: ssl.key ? fs.readFileSync(ssl.key) : undefined,
85
+ cert: ssl.cert ? fs.readFileSync(ssl.cert) : undefined,
86
+ ca: ssl.ca ? fs.readFileSync(ssl.ca) : undefined
86
87
  };
87
88
  if (!options.key || !options.cert) {
88
89
  throw new Error('HTTPS enabled but missing key or cert');
@@ -101,16 +102,18 @@ export default class HTTPModule extends Module {
101
102
  /**
102
103
  * stop http service
103
104
  */
104
- stop() {
105
- if (this.server && this.server.listening)
106
- return new Promise((resolve, reject) => {
107
- this.server.close(function (error) {
108
- if (error)
109
- reject(error);
110
- else
111
- resolve();
112
- });
105
+ async stop() {
106
+ const { server } = this;
107
+ if (!server || !server.listening)
108
+ return Promise.resolve();
109
+ return new Promise((resolve, reject) => {
110
+ server.close(function (error) {
111
+ if (error)
112
+ reject(error);
113
+ else
114
+ resolve();
113
115
  });
116
+ });
114
117
  }
115
118
  /**
116
119
  * browser cross origin
@@ -120,9 +123,14 @@ export default class HTTPModule extends Module {
120
123
  const origin = this.config.origin;
121
124
  const requestOrigin = request.headers.origin;
122
125
  if (origin && requestOrigin) {
123
- if (origin === '*' ||
124
- origin === requestOrigin ||
125
- (Array.isArray(origin) && origin.includes(requestOrigin))) {
126
+ let allow = false;
127
+ if ('string' === typeof origin) {
128
+ allow = origin === '*' || new RegExp(origin).test(requestOrigin);
129
+ }
130
+ else if (Array.isArray(origin)) {
131
+ allow = origin.some(item => item === '*' || new RegExp(item).test(requestOrigin));
132
+ }
133
+ if (allow) {
126
134
  response.setHeader('Access-Control-Allow-Origin', requestOrigin);
127
135
  response.setHeader('Vary', 'Origin');
128
136
  }
@@ -132,7 +140,7 @@ export default class HTTPModule extends Module {
132
140
  return false;
133
141
  }
134
142
  response.setHeader('Access-Control-Max-Age', 3600);
135
- response.setHeader('Access-Control-Allow-Headers', 'x-caller,content-type');
143
+ response.setHeader('Access-Control-Allow-Headers', 'x-caller,x-token,content-type');
136
144
  response.setHeader('Access-Control-Allow-Methods', '*');
137
145
  }
138
146
  if (request.method === 'OPTIONS') {
@@ -143,9 +151,7 @@ export default class HTTPModule extends Module {
143
151
  return true;
144
152
  }
145
153
  async requestHandler(request, response) {
146
- if (!this.cors(request, response))
147
- return;
148
- const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
154
+ const url = new URL(request.url || '', `http://${request.headers.host || 'localhost'}`);
149
155
  const method = request.method || 'GET';
150
156
  // 尝试匹配路由
151
157
  const matched = this.router.match(method, url.pathname);
@@ -170,6 +176,9 @@ export default class HTTPModule extends Module {
170
176
  await this.router.handleRoute(matched.route, matched.params, req, res);
171
177
  return;
172
178
  }
179
+ // 内置cors仅处理RPC请求
180
+ if (!this.cors(request, response))
181
+ return;
173
182
  // 回退到RPC调用
174
183
  if (url.pathname === this.config.path) {
175
184
  await this.call(request, response);
@@ -199,6 +208,24 @@ export default class HTTPModule extends Module {
199
208
  * HTTP-RPC服务器请求监听方法
200
209
  */
201
210
  async call(request, response) {
211
+ // global unique id
212
+ const traceId = String(request.headers['x-trace-id'] || '');
213
+ // service name, required
214
+ const caller = String(request.headers['x-caller'] || 'anonymous');
215
+ // client ip or caller service ip
216
+ const ip = String(request.headers['x-real-ip'] || request.socket.remoteAddress || '');
217
+ // startup client ip
218
+ const sourceIP = String(request.headers['x-source-ip'] || request.socket.remoteAddress || '');
219
+ // check token
220
+ const token = String(request.headers['x-token'] || '');
221
+ const { allow, reason } = oox.checkAllow(ip, caller, token);
222
+ if (!allow)
223
+ return this.respond(request, response, {
224
+ success: false,
225
+ error: {
226
+ message: reason
227
+ }
228
+ });
202
229
  let callArgs = Object.create(null);
203
230
  try {
204
231
  callArgs = await this.getCallArgsFromRequest(request);
@@ -212,14 +239,6 @@ export default class HTTPModule extends Module {
212
239
  }
213
240
  });
214
241
  }
215
- // global unique id
216
- const traceId = String(request.headers['x-trace-id'] || '');
217
- // service name, required
218
- const caller = String(request.headers['x-caller'] || 'anonymous');
219
- // client ip or caller service ip
220
- const ip = String(request.headers['x-ip'] || request.socket.remoteAddress || '');
221
- // startup client ip
222
- const sourceIP = String(request.headers['x-real-ip'] || '');
223
242
  const { action, params = [] } = callArgs;
224
243
  const context = oox.genContext({ traceId, caller, sourceIP, ip, callerId: '' });
225
244
  const format = await oox.call(action, params, context);
@@ -237,15 +256,15 @@ export default class HTTPModule extends Module {
237
256
  try {
238
257
  returnsString = JSON.stringify(returns);
239
258
  }
240
- catch ({ message, stack }) {
259
+ catch (error) {
241
260
  delete returns.body;
242
261
  returns.success = false;
243
262
  returns.error = {
244
- message
263
+ message: error.message
245
264
  };
246
265
  if (oox.config.errorStack) {
247
266
  // 返回错误调用栈信息
248
- returns.error.stack = stack;
267
+ returns.error.stack = error.stack;
249
268
  }
250
269
  returnsString = JSON.stringify(returns);
251
270
  }
@@ -253,34 +272,4 @@ export default class HTTPModule extends Module {
253
272
  response.setHeader('Content-Length', Buffer.byteLength(returnsString));
254
273
  response.end(returnsString);
255
274
  }
256
- /**
257
- * HTTP RPC
258
- */
259
- async rpc(url, action, params, context) {
260
- if (!context || !context.traceId) {
261
- context = oox.getContext();
262
- }
263
- const { traceId, caller, sourceIP } = context;
264
- const headers = {
265
- 'Content-Type': 'application/json',
266
- 'x-trace-id': String(traceId),
267
- };
268
- if (caller)
269
- headers['x-caller'] = String(caller);
270
- if (sourceIP)
271
- headers['x-real-ip'] = sourceIP;
272
- // headers [ 'x-ip' ] = getIPAddress ( 4 ) [ 0 ]
273
- const format = await httpRequest(url, {
274
- headers
275
- }, JSON.stringify({ action, params }));
276
- if ('string' === typeof format)
277
- throw new Error(format);
278
- const { success, error, body } = format;
279
- if (success)
280
- return body;
281
- else if (error)
282
- throw new Error(error.message);
283
- else
284
- throw new Error('[RPC] Unknown Error');
285
- }
286
275
  }
@@ -84,12 +84,17 @@ export default class Router {
84
84
  }
85
85
  // 添加路由
86
86
  addRoute(method, path, handler, middleware = []) {
87
- if (!this.routes.has(method)) {
88
- this.routes.set(method, []);
89
- }
90
87
  // 编译路径为正则表达式,支持路径参数
91
88
  const { regex, paramNames } = this.compilePath(path);
92
- this.routes.get(method).push({
89
+ let routes = [];
90
+ if (this.routes.has(method)) {
91
+ routes = this.routes.get(method) || [];
92
+ }
93
+ else {
94
+ routes = [];
95
+ this.routes.set(method, routes);
96
+ }
97
+ routes.push({
93
98
  path,
94
99
  method,
95
100
  handler,
package/modules/index.js CHANGED
@@ -1,86 +1,90 @@
1
- import Module from './module.js';
2
1
  import HTTP from './http/index.js';
3
2
  import SocketIO from './socketio/index.js';
4
- export default class Modules extends Module {
5
- /**
6
- * the module unique name
7
- */
8
- name = 'oox:modules';
9
- /**
10
- * FIFO queue for modules starting
11
- */
12
- #queue = [];
13
- /**
14
- * all modules map
15
- */
16
- #map = new Map();
17
- /**
18
- * all builtin modules
19
- */
20
- builtins = {
21
- http: new HTTP,
22
- socketio: new SocketIO,
23
- };
24
- constructor() {
25
- super();
26
- this.add(this.builtins.http);
27
- this.add(this.builtins.socketio);
28
- }
29
- add(module) {
30
- if (!module.name || 'string' !== typeof module.name)
31
- throw new Error(`Module<${module.name}> has noname`);
32
- if (this.#map.has(module.name))
33
- throw new Error(`Module<${module.name}> has exists`);
34
- this.#map.set(module.name, module);
35
- this.#queue.push(module);
36
- return this;
37
- }
38
- get(name) {
39
- return this.#map.get(name);
40
- }
41
- async remove(name) {
42
- const module = this.get(name);
43
- if (!module)
44
- return;
45
- await module.stop();
46
- const index = this.#queue.indexOf(module);
47
- this.#queue.splice(index, 1);
48
- this.#map.delete(name);
49
- }
50
- setConfig(config) {
51
- for (const module of this.#queue) {
52
- if (module.name in config) {
53
- const moduleConfig = config[module.name];
54
- if ('boolean' === typeof moduleConfig) {
55
- // eg: http=yes/no
56
- module.setConfig({ enabled: moduleConfig });
57
- }
58
- else {
59
- module.setConfig(moduleConfig);
60
- }
3
+ /**
4
+ * FIFO queue for modules starting
5
+ */
6
+ export const queue = [];
7
+ /**
8
+ * all modules map
9
+ */
10
+ export const map = new Map();
11
+ /**
12
+ * add module to modules queue
13
+ */
14
+ export function add(module) {
15
+ if (!module.name || 'string' !== typeof module.name)
16
+ throw new Error(`Module<${module.name}> has noname`);
17
+ if (map.has(module.name))
18
+ throw new Error(`Module<${module.name}> has exists`);
19
+ map.set(module.name, module);
20
+ queue.push(module);
21
+ }
22
+ /**
23
+ * get module by name
24
+ */
25
+ export function get(name) {
26
+ return map.get(name) || null;
27
+ }
28
+ export async function remove(name) {
29
+ const module = map.get(name);
30
+ if (!module)
31
+ return;
32
+ await module.stop();
33
+ const index = queue.indexOf(module);
34
+ queue.splice(index, 1);
35
+ map.delete(name);
36
+ }
37
+ /**
38
+ * set modules config
39
+ */
40
+ export function setConfig(config) {
41
+ for (const module of queue) {
42
+ if (module.name in config) {
43
+ const moduleConfig = config[module.name];
44
+ if ('boolean' === typeof moduleConfig) {
45
+ // eg: http=yes/no
46
+ module.setConfig({ enabled: moduleConfig });
61
47
  }
62
48
  else {
63
- module.setConfig({ enabled: true });
49
+ module.setConfig(moduleConfig);
64
50
  }
65
- config[module.name] = module.getConfig();
66
51
  }
52
+ else {
53
+ module.setConfig({ enabled: true });
54
+ }
55
+ config[module.name] = module.getConfig();
67
56
  }
68
- async serve() {
69
- try {
70
- for (const module of this.#queue) {
71
- if (module.getConfig().enabled) {
72
- await module.serve();
73
- }
57
+ }
58
+ /**
59
+ * serve modules
60
+ */
61
+ export async function serve() {
62
+ try {
63
+ for (const module of queue) {
64
+ if (module.getConfig().enabled) {
65
+ await module.serve();
74
66
  }
75
67
  }
76
- catch (error) {
77
- await this.stop();
78
- throw error;
79
- }
80
68
  }
81
- async stop() {
82
- for (const module of this.#queue) {
83
- await module.stop();
84
- }
69
+ catch (error) {
70
+ await stop();
71
+ throw error;
72
+ }
73
+ }
74
+ /**
75
+ * stop all modules
76
+ */
77
+ export async function stop() {
78
+ for (const module of queue) {
79
+ await module.stop();
85
80
  }
86
81
  }
82
+ /**
83
+ * all builtin modules
84
+ */
85
+ export const builtins = {
86
+ http: new HTTP,
87
+ socketio: new SocketIO,
88
+ };
89
+ add(builtins.http);
90
+ add(builtins.socketio);
package/modules/module.js CHANGED
@@ -1,11 +1,2 @@
1
- export class ModuleConfig {
2
- enabled = true;
3
- }
4
1
  export default class Module {
5
- config;
6
- name;
7
- setConfig(config) { }
8
- getConfig() { return this.config; }
9
- async serve() { }
10
- async stop() { }
11
2
  }
@@ -1,158 +1,49 @@
1
1
  import * as SocketIOClient from 'socket.io-client';
2
2
  import * as oox from '../../index.js';
3
- import { OOXEvent, genWebSocketURL, isWebSocketURL } from './utils.js';
3
+ import { OOXEvent, genWebSocketURL } from './utils.js';
4
4
  import { randomUUID } from 'node:crypto';
5
- export default class SocketIOAdapter {
6
- /**
7
- * 客户端发送连接列表
8
- * @param datas
9
- */
10
- [OOXEvent.SYNC_CONNECTIONS](datas) {
11
- for (const data of datas) {
12
- if (data.name === oox.config.name)
13
- continue;
14
- if (!data.url || !isWebSocketURL(data.url))
15
- continue;
16
- const has = oox.keepAliveConnections.has(data.name, data.id);
17
- if (has)
18
- continue;
19
- this.open(data.url).catch((error) => console.error(error));
20
- }
21
- }
22
- bindClientConnectionEvents(connection) {
23
- const socket = connection.nativeConnection;
24
- socket.on(OOXEvent.CONNECTED, ({ id, name }) => {
25
- connection.updateIdAndName(id, name);
26
- connection.enabled = true;
27
- socket.emit(OOXEvent.SYNC_CONNECTIONS, this[OOXEvent.SYNC_CONNECTIONS].bind(this));
28
- });
29
- socket.on(OOXEvent.DISABLED, () => {
30
- connection.enabled = false;
31
- });
32
- socket.on('disconnect', (reason) => {
33
- connection.emit('disconnect');
34
- oox.removeKeepAliveConnection(connection);
35
- socket.removeAllListeners();
36
- socket.disconnect();
37
- });
38
- socket.on('connect', () => {
39
- connection.emit('connect');
40
- });
41
- socket.on('connect_error', (error) => {
42
- connection.emit('error', error);
43
- oox.removeKeepAliveConnection(connection);
44
- socket.removeAllListeners();
45
- socket.disconnect();
46
- });
47
- }
48
- newConnection(url) {
49
- const { id, host, name } = oox.config;
5
+ import { SampleKeepAliveConnectionAdapter } from '../../samples/index.js';
6
+ export default class SocketIOAdapter extends SampleKeepAliveConnectionAdapter {
7
+ name = 'socketio';
8
+ OOXEvent = OOXEvent;
9
+ nativeEvent = { CONNECT: 'connect', DISCONNECT: 'disconnect', ERROR: 'connect_error' };
10
+ newConnection(identify) {
11
+ const { id, name } = oox.config;
50
12
  const headers = {
51
- 'x-ip': host,
52
13
  'x-caller': name,
53
14
  'x-caller-id': id,
54
15
  };
55
16
  let mURL;
56
- if ('string' === typeof url) {
57
- mURL = genWebSocketURL(url);
17
+ const connectionData = {
18
+ name: 'anonymous',
19
+ id: randomUUID(),
20
+ adapter: this.name,
21
+ ip: '',
22
+ token: ''
23
+ };
24
+ if ('string' === typeof identify) {
25
+ mURL = genWebSocketURL(identify);
26
+ }
27
+ else if (identify instanceof URL) {
28
+ mURL = identify;
29
+ }
30
+ else if (identify.url) {
31
+ // KeepAliveConnectionData
32
+ Object.assign(connectionData, identify);
33
+ mURL = new URL(identify.url);
34
+ if (identify.token) {
35
+ headers['x-token'] = identify.token;
36
+ }
58
37
  }
59
38
  else {
60
- mURL = url;
39
+ throw new Error('identify must be string, URL, or KeepAliveConnectionData');
61
40
  }
62
41
  const socket = SocketIOClient.io(mURL.origin, {
63
42
  extraHeaders: headers,
64
43
  path: mURL.pathname,
65
44
  autoConnect: false,
66
45
  });
67
- const data = {
68
- name: 'anonymous',
69
- id: randomUUID(),
70
- host: mURL.host,
71
- url: mURL
72
- };
73
- const connection = new oox.RPCKeepAliveConnection(this, socket, data);
74
- return connection;
75
- }
76
- async open(url) {
77
- const connection = this.newConnection(url);
78
- oox.addKeepAliveConnection(connection);
79
- this.bindClientConnectionEvents(connection);
80
- this.bindCall(connection);
81
- await new Promise((resolve, reject) => {
82
- connection.once('enabled', resolve);
83
- connection.once('error', reject);
84
- connection.once('disconnect', () => {
85
- reject(new Error('disconnect'));
86
- });
87
- connection.nativeConnection.connect();
88
- });
46
+ const connection = new oox.KeepAliveConnection(this, socket, connectionData);
89
47
  return connection;
90
48
  }
91
- async close(connection) {
92
- connection.nativeConnection.disconnect();
93
- return Promise.resolve();
94
- }
95
- /**
96
- * socketio emit
97
- */
98
- async emit(socket, event, params) {
99
- if (!socket.connected)
100
- throw new Error('Socket not connected');
101
- return await new Promise((resolve, reject) => {
102
- const onError = (reason) => {
103
- const message = 'string' === typeof reason ? reason : reason instanceof Error ? reason.message : 'connect error';
104
- reject(new Error(message));
105
- };
106
- // RPC 执行时中断连接
107
- socket.once('disconnect', onError);
108
- socket.emit(event, ...params, (returns) => {
109
- socket.offAny(onError);
110
- resolve(returns);
111
- });
112
- });
113
- }
114
- /**
115
- * RPC
116
- */
117
- async rpc(connection, action, params, context) {
118
- if (!context)
119
- context = oox.getContext();
120
- const { success, error, body } = await this.emit(connection.nativeConnection, 'call', [action, params, context]);
121
- if (success)
122
- return body;
123
- else if (error)
124
- throw new Error(error.message);
125
- else
126
- throw new Error('[RPC] Unknown Error');
127
- }
128
- /**
129
- * 绑定 call 事件
130
- * @param connection
131
- */
132
- bindCall(connection) {
133
- const { id, name, host } = connection.data;
134
- const connectionContext = {
135
- sourceIP: '',
136
- ip: host,
137
- caller: name,
138
- callerId: id,
139
- connection
140
- };
141
- connection.nativeConnection.on('call', async (action, params, context, callback) => {
142
- if ('object' !== typeof context)
143
- context = oox.genContext(connectionContext);
144
- else
145
- context = oox.genContext(Object.assign(context, connectionContext));
146
- this.call(action, params, context, callback);
147
- });
148
- }
149
- async call(action, params, context, callback) {
150
- const returns = await oox.call(action, params, context);
151
- if (returns.error && !oox.config.errorStack) {
152
- // 不返回错误调用栈信息
153
- delete returns.error.stack;
154
- }
155
- 'function' === typeof callback && callback(returns);
156
- return returns;
157
- }
158
49
  }