badmfck-api-server 3.9.91 → 3.9.94
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/apiServer/APIService.d.ts +2 -2
- package/dist/apiServer/APIService.js +80 -50
- package/dist/apiServer/BaseEndpoint.d.ts +9 -1
- package/dist/apiServer/BaseEndpoint.js +149 -42
- package/dist/apiServer/LocalRequest.js +0 -26
- package/dist/apiServer/external/ExternalService.js +14 -26
- package/dist/apiServer/external/MicroserviceClient.d.ts +4 -0
- package/dist/apiServer/external/MicroserviceClient.js +10 -0
- package/dist/apiServer/external/MicroserviceHost.d.ts +4 -0
- package/dist/apiServer/external/MicroserviceHost.js +10 -0
- package/dist/apiServer/http/HReq.d.ts +0 -0
- package/dist/apiServer/http/HReq.js +1 -0
- package/dist/apiServer/http/HTTP.d.ts +97 -0
- package/dist/apiServer/http/HTTP.js +356 -0
- package/dist/apiServer/monitor/Monitor.js +2 -34
- package/dist/apiServer/structures/Interfaces.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/package.json +3 -2
|
@@ -23,7 +23,7 @@ export interface APIServiceNetworkLogItem {
|
|
|
23
23
|
error?: string | null;
|
|
24
24
|
}
|
|
25
25
|
export interface IInterceptor<T> {
|
|
26
|
-
intercept(req: HTTPRequestVO<any
|
|
26
|
+
intercept(req: HTTPRequestVO<any>, internalCallParams: any): Promise<T | IError>;
|
|
27
27
|
}
|
|
28
28
|
export interface APIServiceOptions<TInterceptor = unknown> {
|
|
29
29
|
port: number;
|
|
@@ -58,7 +58,7 @@ export declare const REQ_INTERNAL_CALL: Req<HTTPRequestVO, IError | any>;
|
|
|
58
58
|
export declare const REQ_MONITOR_USERS: Req<void, IMonitorUser[]>;
|
|
59
59
|
export declare const REQ_DOC_USERS: Req<void, IMonitorUser[]>;
|
|
60
60
|
export declare function Initializer(services: IBaseService[]): Promise<void>;
|
|
61
|
-
export declare class APIService extends BaseService {
|
|
61
|
+
export declare class APIService<TInterceptorResult = any, TInternalCallParams = any> extends BaseService {
|
|
62
62
|
private version;
|
|
63
63
|
private options;
|
|
64
64
|
private monitor;
|
|
@@ -29,7 +29,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.APIService = exports.Initializer = exports.REQ_DOC_USERS = exports.REQ_MONITOR_USERS = exports.REQ_INTERNAL_CALL = exports.REQ_HTTP_SERVER = exports.REQ_HTTP_REQUESTS_COUNT = exports.REQ_HTTP_LOG = exports.getDefaultOptions = void 0;
|
|
30
30
|
const express_1 = __importDefault(require("express"));
|
|
31
31
|
const BaseService_1 = require("./BaseService");
|
|
32
|
-
const cors_1 = __importDefault(require("cors"));
|
|
33
32
|
const BaseEndpoint_1 = require("./BaseEndpoint");
|
|
34
33
|
const DefaultErrors_1 = __importStar(require("./structures/DefaultErrors"));
|
|
35
34
|
const badmfck_signal_1 = require("badmfck-signal");
|
|
@@ -50,7 +49,7 @@ function defaultOptions() {
|
|
|
50
49
|
return {
|
|
51
50
|
port: 8080,
|
|
52
51
|
baseEndPoint: "/api/",
|
|
53
|
-
corsHostWhiteList: ["localhost:3000"],
|
|
52
|
+
corsHostWhiteList: ["http://localhost:3000"],
|
|
54
53
|
endpoints: [],
|
|
55
54
|
jsonLimit: "10mb",
|
|
56
55
|
projectName: "Application project",
|
|
@@ -77,7 +76,7 @@ exports.REQ_HTTP_REQUESTS_COUNT = new badmfck_signal_1.Req(undefined, "REQ_HTTP_
|
|
|
77
76
|
exports.REQ_HTTP_SERVER = new badmfck_signal_1.Req(undefined, "REQ_HTTP_SERVER");
|
|
78
77
|
exports.REQ_INTERNAL_CALL = new badmfck_signal_1.Req(undefined, "REQ_INTERNAL_CALL");
|
|
79
78
|
exports.REQ_MONITOR_USERS = new badmfck_signal_1.Req(undefined, "REQ_MONITOR_USERS");
|
|
80
|
-
exports.REQ_DOC_USERS = new badmfck_signal_1.Req(undefined, "
|
|
79
|
+
exports.REQ_DOC_USERS = new badmfck_signal_1.Req(undefined, "REQ_DOC_USERS");
|
|
81
80
|
const activeServices = [];
|
|
82
81
|
async function Initializer(services) {
|
|
83
82
|
services.push(new StatService_1.StatService());
|
|
@@ -92,7 +91,7 @@ async function Initializer(services) {
|
|
|
92
91
|
}
|
|
93
92
|
exports.Initializer = Initializer;
|
|
94
93
|
class APIService extends BaseService_1.BaseService {
|
|
95
|
-
version = "3.9.
|
|
94
|
+
version = "3.9.94";
|
|
96
95
|
options;
|
|
97
96
|
monitor = null;
|
|
98
97
|
started = new Date();
|
|
@@ -105,7 +104,18 @@ class APIService extends BaseService_1.BaseService {
|
|
|
105
104
|
this.options.corsHostWhiteList = [];
|
|
106
105
|
const self = "http://localhost:" + this.options.port;
|
|
107
106
|
if (!this.options.corsHostWhiteList.find(val => val === self))
|
|
108
|
-
this.options.corsHostWhiteList.push();
|
|
107
|
+
this.options.corsHostWhiteList.push(self);
|
|
108
|
+
const list = [];
|
|
109
|
+
for (let h of this.options.corsHostWhiteList) {
|
|
110
|
+
h = h.replace(/\/$/, "");
|
|
111
|
+
if (!/^https?:\/\//i.test(h)) {
|
|
112
|
+
list.push(`http://${h}`, `https://${h}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
list.push(h);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.options.corsHostWhiteList = Array.from(new Set(list));
|
|
109
119
|
exports.REQ_MONITOR_USERS.listener = async () => this.options.access.monitor ?? [];
|
|
110
120
|
exports.REQ_DOC_USERS.listener = async () => this.options.access.documentation ?? [];
|
|
111
121
|
this.options.endpoints.push(new Monitor_1.Monitor(this.options));
|
|
@@ -142,8 +152,8 @@ class APIService extends BaseService_1.BaseService {
|
|
|
142
152
|
exports.REQ_HTTP_SERVER.listener = async (_) => { return { express: app, http: server }; };
|
|
143
153
|
if (this.options.isProductionEnvironment)
|
|
144
154
|
app.set("env", 'production');
|
|
145
|
-
app.use(express_1.default.json({ limit: '10mb' }));
|
|
146
|
-
app.use(express_1.default.urlencoded({ limit: '10mb', extended: true }));
|
|
155
|
+
app.use(express_1.default.json({ limit: this.options.jsonLimit ?? '10mb' }));
|
|
156
|
+
app.use(express_1.default.urlencoded({ limit: this.options.jsonLimit ?? '10mb', extended: true }));
|
|
147
157
|
app.use((0, express_fileupload_1.default)({
|
|
148
158
|
limitHandler: (req, res, next) => {
|
|
149
159
|
this.sendResponse(req.get("Referer") ?? "", res, {
|
|
@@ -161,6 +171,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
161
171
|
response: undefined,
|
|
162
172
|
files: undefined
|
|
163
173
|
});
|
|
174
|
+
return;
|
|
164
175
|
},
|
|
165
176
|
limits: { fileSize: this.options.fileLimit },
|
|
166
177
|
useTempFiles: true,
|
|
@@ -168,7 +179,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
168
179
|
tempFileDir: this.options.fileTempDir,
|
|
169
180
|
abortOnLimit: true
|
|
170
181
|
}));
|
|
171
|
-
app.use(
|
|
182
|
+
app.use((err, req, resp, next) => {
|
|
172
183
|
if (!err) {
|
|
173
184
|
next();
|
|
174
185
|
return;
|
|
@@ -193,15 +204,35 @@ class APIService extends BaseService_1.BaseService {
|
|
|
193
204
|
response: undefined,
|
|
194
205
|
files: undefined
|
|
195
206
|
});
|
|
207
|
+
return;
|
|
208
|
+
});
|
|
209
|
+
const corsSet = new Set(this.options.corsHostWhiteList.map(x => String(x).replace(/\/$/, "")));
|
|
210
|
+
app.use((req, res, next) => {
|
|
211
|
+
const originHeader = req.headers.origin;
|
|
212
|
+
if (!originHeader)
|
|
213
|
+
return next();
|
|
214
|
+
let originNorm;
|
|
215
|
+
try {
|
|
216
|
+
originNorm = new URL(String(originHeader)).origin;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
res.status(403).send({
|
|
220
|
+
error: { ...DefaultErrors_1.default.FORBIDDEN, details: "Invalid Origin header" },
|
|
221
|
+
data: null,
|
|
222
|
+
httpStatus: 403,
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!corsSet.has(originNorm)) {
|
|
227
|
+
res.status(403).send({
|
|
228
|
+
error: { ...DefaultErrors_1.default.FORBIDDEN, details: `Origin not allowed: ${originNorm}` },
|
|
229
|
+
data: null,
|
|
230
|
+
httpStatus: 403,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
return next();
|
|
196
235
|
});
|
|
197
|
-
const corsOptions = {
|
|
198
|
-
origin: (origin, callback) => {
|
|
199
|
-
const originIsWhitelisted = this.options.corsHostWhiteList.includes(origin);
|
|
200
|
-
callback(null, originIsWhitelisted);
|
|
201
|
-
},
|
|
202
|
-
credentials: true
|
|
203
|
-
};
|
|
204
|
-
app.use((0, cors_1.default)(corsOptions));
|
|
205
236
|
BaseEndpoint_1.BaseEndpoint.setEntryPoint(this.options.baseEndPoint);
|
|
206
237
|
for (let i of this.options.endpoints) {
|
|
207
238
|
await i.init();
|
|
@@ -214,17 +245,17 @@ class APIService extends BaseService_1.BaseService {
|
|
|
214
245
|
this.requestsCount++;
|
|
215
246
|
const tme = +new Date();
|
|
216
247
|
const execute = async () => {
|
|
217
|
-
const body = req.body;
|
|
248
|
+
const body = { ...(req.body ?? {}) };
|
|
218
249
|
if (req.query && typeof req.query === "object") {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
const keys = Object.keys(req.query);
|
|
251
|
+
for (let k of keys)
|
|
252
|
+
body[k] = req.query[k];
|
|
222
253
|
}
|
|
223
254
|
let httpRequest = {
|
|
224
255
|
raw: req,
|
|
225
256
|
response: res,
|
|
226
257
|
method: req.method,
|
|
227
|
-
data:
|
|
258
|
+
data: body,
|
|
228
259
|
params: req.params,
|
|
229
260
|
headers: req.headers,
|
|
230
261
|
endpoint: ep,
|
|
@@ -273,7 +304,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
273
304
|
if (!ignoreInterceptor) {
|
|
274
305
|
let interceptorResult;
|
|
275
306
|
if (this.options.interceptor) {
|
|
276
|
-
interceptorResult = await this.options.interceptor.intercept(httpRequest);
|
|
307
|
+
interceptorResult = await this.options.interceptor.intercept(httpRequest, j.internalCallParams);
|
|
277
308
|
if (DefaultErrors_1.ErrorUtils.isError(interceptorResult) && !allowInterceptorError) {
|
|
278
309
|
this.sendResponse(req.get("Referer") ?? "", res, interceptorResult, tme, ep, httpRequest);
|
|
279
310
|
return;
|
|
@@ -324,7 +355,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
324
355
|
}
|
|
325
356
|
this.sendResponse(req.get("Referer") ?? "", res, result, tme, ep, httpRequest);
|
|
326
357
|
};
|
|
327
|
-
execute();
|
|
358
|
+
return execute();
|
|
328
359
|
});
|
|
329
360
|
}
|
|
330
361
|
}
|
|
@@ -359,15 +390,17 @@ class APIService extends BaseService_1.BaseService {
|
|
|
359
390
|
endpoint: req?.endpoint,
|
|
360
391
|
headers: req?.headers,
|
|
361
392
|
method: req?.method,
|
|
362
|
-
data: req?.data,
|
|
393
|
+
data: this.checkDataLength(req?.data),
|
|
363
394
|
params: req?.params,
|
|
364
395
|
interceptorResult: req?.interceptorResult
|
|
365
396
|
},
|
|
366
|
-
response: data
|
|
397
|
+
response: this.checkDataLength(data),
|
|
367
398
|
};
|
|
368
399
|
this.netLog.push(logItem);
|
|
369
400
|
if (this.netLog.length > 100)
|
|
370
401
|
this.netLog.shift();
|
|
402
|
+
if (nextLogID > Number.MAX_SAFE_INTEGER - 1000)
|
|
403
|
+
nextLogID = 0;
|
|
371
404
|
}
|
|
372
405
|
async sendResponse(ref, res, data, requestTime, endpoint, req) {
|
|
373
406
|
if (data.blockResponse) {
|
|
@@ -392,7 +425,7 @@ class APIService extends BaseService_1.BaseService {
|
|
|
392
425
|
if (this.options.appVersion)
|
|
393
426
|
data.version = this.options.appVersion;
|
|
394
427
|
MonitorService_1.S_STAT_REGISTRATE_REQUEST.invoke(data);
|
|
395
|
-
if (res.destroyed || res.
|
|
428
|
+
if (res.socket?.destroyed || res.writableEnded || res.headersSent || (res.destroyed !== undefined && res.destroyed)) {
|
|
396
429
|
(0, LogService_1.logAPI)("Connection already closed, can't send response for: " + data.endpoint);
|
|
397
430
|
}
|
|
398
431
|
else {
|
|
@@ -401,11 +434,17 @@ class APIService extends BaseService_1.BaseService {
|
|
|
401
434
|
res.sendFile(data.file, err => {
|
|
402
435
|
if (err) {
|
|
403
436
|
(0, LogService_1.logError)("Can't send file: " + data.file);
|
|
404
|
-
|
|
405
|
-
error: DefaultErrors_1.default.CANT_SEND_FILE,
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
437
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
438
|
+
res.status(500).send({ error: DefaultErrors_1.default.CANT_SEND_FILE, data: null });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
try {
|
|
443
|
+
res.destroy?.(err);
|
|
444
|
+
}
|
|
445
|
+
catch { }
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
409
448
|
}
|
|
410
449
|
else {
|
|
411
450
|
if (req)
|
|
@@ -447,27 +486,18 @@ class APIService extends BaseService_1.BaseService {
|
|
|
447
486
|
this.addNetlog(data, req, requestTime, 0);
|
|
448
487
|
}
|
|
449
488
|
checkDataLength(data, result, lvl) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
489
|
+
let json = "";
|
|
490
|
+
try {
|
|
491
|
+
json = JSON.stringify(data, (k, v) => {
|
|
492
|
+
if (typeof v === 'string' && v.length > 255)
|
|
493
|
+
return v.slice(0, 255) + `...(${v.length})`;
|
|
494
|
+
return v;
|
|
495
|
+
});
|
|
456
496
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
result = {};
|
|
460
|
-
let arrcnt = 100;
|
|
461
|
-
for (let i in data) {
|
|
462
|
-
result[i] = this.checkDataLength(data[i], result[i], lvl + 1);
|
|
463
|
-
arrcnt--;
|
|
464
|
-
if (arrcnt <= 0) {
|
|
465
|
-
result["..."] = "... total: " + Array.isArray(data) ? data.length : Object.keys(data).length;
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
497
|
+
catch (e) {
|
|
498
|
+
json = "[unserializable data]";
|
|
469
499
|
}
|
|
470
|
-
return
|
|
500
|
+
return json.length > 500 ? json.slice(0, 500) + `...(total ${json.length})` : json;
|
|
471
501
|
}
|
|
472
502
|
}
|
|
473
503
|
exports.APIService = APIService;
|
|
@@ -11,6 +11,7 @@ type HTTPMethod = typeof httpMethods[number];
|
|
|
11
11
|
export type IHandler = Record<HTTPMethod, ((req: HTTPRequestVO) => Promise<TransferPacketVO<any>>) | {
|
|
12
12
|
controller: (req: HTTPRequestVO) => Promise<TransferPacketVO<any>>;
|
|
13
13
|
ignoreInterceptor?: boolean;
|
|
14
|
+
internalCallParams?: any;
|
|
14
15
|
allowInterceptorError?: boolean;
|
|
15
16
|
validationModel?: any;
|
|
16
17
|
resultModel?: any;
|
|
@@ -24,6 +25,7 @@ export interface IEndpointHandler {
|
|
|
24
25
|
endpoint: string;
|
|
25
26
|
handler?: ((req: HTTPRequestVO) => Promise<TransferPacketVO<any>>) | IHandler;
|
|
26
27
|
ignoreInterceptor?: boolean;
|
|
28
|
+
internalCallParams?: any;
|
|
27
29
|
ignoreInDocumentation?: boolean;
|
|
28
30
|
ignoreHttpLogging?: boolean;
|
|
29
31
|
independed?: boolean;
|
|
@@ -42,6 +44,7 @@ export declare class BaseEndpoint implements IBaseEndpoint {
|
|
|
42
44
|
ignoreInDocumentation: boolean;
|
|
43
45
|
private static entrypoint;
|
|
44
46
|
endpoint: string;
|
|
47
|
+
protected streamOptions: Record<string, any>;
|
|
45
48
|
static setEntryPoint: (ep: string) => void;
|
|
46
49
|
static getEntryPoint: () => string;
|
|
47
50
|
constructor(endpoint: string);
|
|
@@ -51,6 +54,11 @@ export declare class BaseEndpoint implements IBaseEndpoint {
|
|
|
51
54
|
__precheck(req: HTTPRequestVO): Promise<TransferPacketVO<any> | null>;
|
|
52
55
|
__execute(req: HTTPRequestVO): Promise<TransferPacketVO<any>>;
|
|
53
56
|
protected validateStructure(structure: any, req: HTTPRequestVO): Promise<void>;
|
|
54
|
-
protected createStream(req: HTTPRequestVO
|
|
57
|
+
protected createStream(req: HTTPRequestVO, opts: {
|
|
58
|
+
contentType?: string;
|
|
59
|
+
headers?: {
|
|
60
|
+
[key: string]: string;
|
|
61
|
+
};
|
|
62
|
+
}): HTTPRequestVO;
|
|
55
63
|
}
|
|
56
64
|
export {};
|
|
@@ -17,6 +17,7 @@ class BaseEndpoint {
|
|
|
17
17
|
ignoreInDocumentation = false;
|
|
18
18
|
static entrypoint = "/";
|
|
19
19
|
endpoint = "";
|
|
20
|
+
streamOptions = {};
|
|
20
21
|
static setEntryPoint = (ep) => {
|
|
21
22
|
this.entrypoint = ep;
|
|
22
23
|
if (!this.entrypoint.endsWith("/")) {
|
|
@@ -73,43 +74,65 @@ class BaseEndpoint {
|
|
|
73
74
|
for (let i of this.endpoints) {
|
|
74
75
|
let targetEP = BaseEndpoint.entrypoint + i.endpoint;
|
|
75
76
|
targetEP = targetEP.replaceAll("//", "/");
|
|
76
|
-
if (targetEP
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
if (targetEP !== req.endpoint)
|
|
78
|
+
continue;
|
|
79
|
+
const httpMethod = req.method.toUpperCase();
|
|
80
|
+
const resolveAsStream = () => {
|
|
81
|
+
if (i.asStream)
|
|
82
|
+
return true;
|
|
83
|
+
if (i.handler && typeof i.handler === "object") {
|
|
84
|
+
const h = i.handler[httpMethod];
|
|
85
|
+
if (h && typeof h === "object" && "asStream" in h && h.asStream)
|
|
86
|
+
return true;
|
|
83
87
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
const asStream = resolveAsStream();
|
|
91
|
+
if (i.handler && typeof i.handler === "function") {
|
|
92
|
+
if (req.data && req.data.validationModel && req.data.validationModel === "1")
|
|
93
|
+
return { data: i.validationModel };
|
|
94
|
+
if (i.validationModel)
|
|
95
|
+
await this.validateStructure(i.validationModel, req);
|
|
96
|
+
if (asStream)
|
|
97
|
+
req = this.createStream(req, this.streamOptions[i.endpoint] ?? {});
|
|
98
|
+
const result = await i.handler.call(this, req);
|
|
99
|
+
if (asStream)
|
|
100
|
+
return { blockResponse: true, data: null };
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
if (i.handler && i.handler[httpMethod] && typeof i.handler[httpMethod] === "function") {
|
|
104
|
+
if (req.data && req.data.validationModel && req.data.validationModel === "1")
|
|
105
|
+
return { data: i.validationModel };
|
|
106
|
+
if (i.validationModel)
|
|
107
|
+
await this.validateStructure(i.validationModel, req);
|
|
108
|
+
if (asStream)
|
|
109
|
+
req = this.createStream(req, this.streamOptions[i.endpoint] ?? {});
|
|
110
|
+
const result = await i.handler[httpMethod].call(this, req);
|
|
111
|
+
if (asStream)
|
|
112
|
+
return { blockResponse: true, data: null };
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
if (i.handler
|
|
116
|
+
&& i.handler[httpMethod]
|
|
117
|
+
&& typeof i.handler[httpMethod] === "object"
|
|
118
|
+
&& "controller" in i.handler[httpMethod]
|
|
119
|
+
&& typeof i.handler[httpMethod].controller === "function") {
|
|
120
|
+
const vmodel = i.handler[httpMethod].validationModel;
|
|
121
|
+
if (req.data && req.data.validationModel && req.data.validationModel === "1")
|
|
122
|
+
return { data: vmodel };
|
|
123
|
+
if (vmodel)
|
|
124
|
+
await this.validateStructure(vmodel, req);
|
|
125
|
+
if (asStream)
|
|
126
|
+
req = this.createStream(req, this.streamOptions[i.endpoint] ?? {});
|
|
127
|
+
const result = await i.handler[httpMethod].controller.call(this, req);
|
|
128
|
+
if (asStream)
|
|
129
|
+
return { blockResponse: true, data: null };
|
|
130
|
+
return result;
|
|
112
131
|
}
|
|
132
|
+
return {
|
|
133
|
+
error: { ...DefaultErrors_1.default.METHOD_NOT_ALLOWED, details: "No handler for " + httpMethod },
|
|
134
|
+
data: null
|
|
135
|
+
};
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
(0, LogService_1.logWarn)("${BaseEndpoint.js}", "Unhandled entrypoint: " + this.endpoint);
|
|
@@ -128,17 +151,101 @@ class BaseEndpoint {
|
|
|
128
151
|
throw { ...DefaultErrors_1.default.WRONG_PARAMS, details: report };
|
|
129
152
|
Validator_1.Validator.filterStructure(structure, req.data);
|
|
130
153
|
}
|
|
131
|
-
createStream(req) {
|
|
154
|
+
createStream(req, opts) {
|
|
132
155
|
const res = req.response;
|
|
133
|
-
res.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
if (res.headersSent)
|
|
157
|
+
throw { ...DefaultErrors_1.default.INTERNAL_SERVER_ERROR, details: "Headers already sent, can't create stream" };
|
|
158
|
+
res.statusCode = 200;
|
|
159
|
+
res.setHeader("Content-Type", opts?.contentType ?? "application/octet-stream");
|
|
160
|
+
res.setHeader("Transfer-Encoding", "chunked");
|
|
161
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
162
|
+
if (opts?.headers) {
|
|
163
|
+
for (let h in opts.headers) {
|
|
164
|
+
res.setHeader(h, opts.headers[h]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const stream = new stream_1.Readable({
|
|
168
|
+
read() {
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
let finished = false;
|
|
172
|
+
let timer;
|
|
173
|
+
const cleanup = () => {
|
|
174
|
+
if (finished)
|
|
175
|
+
return;
|
|
176
|
+
finished = true;
|
|
177
|
+
if (timer)
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
try {
|
|
180
|
+
if (!stream.destroyed) {
|
|
181
|
+
stream.destroy();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
console.error("Error during stream cleanup:", e);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
if (!res.writableEnded)
|
|
189
|
+
res.end();
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
};
|
|
193
|
+
const endOk = () => {
|
|
194
|
+
if (finished)
|
|
195
|
+
return;
|
|
196
|
+
finished = true;
|
|
197
|
+
if (timer)
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
try {
|
|
200
|
+
if (!stream.destroyed)
|
|
201
|
+
stream.push(null);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
cleanup();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
res.on('close', () => {
|
|
208
|
+
cleanup();
|
|
209
|
+
});
|
|
210
|
+
const setupTimer = () => {
|
|
211
|
+
if (finished)
|
|
212
|
+
return;
|
|
213
|
+
if (timer)
|
|
214
|
+
clearTimeout(timer);
|
|
215
|
+
timer = setTimeout(() => cleanup(), 60_000);
|
|
216
|
+
};
|
|
217
|
+
setupTimer();
|
|
218
|
+
const origPush = stream.push.bind(stream);
|
|
219
|
+
stream.push = (chunk, encoding) => {
|
|
220
|
+
if (chunk === null)
|
|
221
|
+
return origPush(null);
|
|
222
|
+
if (finished)
|
|
223
|
+
return false;
|
|
224
|
+
setupTimer();
|
|
225
|
+
return origPush(chunk, encoding);
|
|
226
|
+
};
|
|
227
|
+
req.raw?.on?.("aborted", () => {
|
|
228
|
+
cleanup();
|
|
229
|
+
try {
|
|
230
|
+
res.destroy?.();
|
|
231
|
+
}
|
|
232
|
+
catch { }
|
|
233
|
+
});
|
|
234
|
+
req.raw?.on?.("close", cleanup);
|
|
137
235
|
stream.on('error', (err) => {
|
|
138
|
-
console.error(
|
|
139
|
-
|
|
236
|
+
console.error("Stream error:", err);
|
|
237
|
+
cleanup();
|
|
238
|
+
try {
|
|
239
|
+
res.destroy?.(err);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
res.end();
|
|
243
|
+
}
|
|
140
244
|
});
|
|
245
|
+
res.on("finish", cleanup);
|
|
246
|
+
stream.pipe(res);
|
|
141
247
|
req.stream = stream;
|
|
248
|
+
req.endStream = (force) => force ? cleanup() : endOk();
|
|
142
249
|
return req;
|
|
143
250
|
}
|
|
144
251
|
}
|
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.LocalRequest = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
4
|
const LocalRequest = async (ep, data, method, headers) => {
|
|
9
|
-
let m = "get";
|
|
10
|
-
if (method)
|
|
11
|
-
m = method.toLowerCase();
|
|
12
|
-
if (data && !method)
|
|
13
|
-
m = "post";
|
|
14
|
-
if (ep.startsWith("/"))
|
|
15
|
-
ep = ep.substring(1);
|
|
16
|
-
let url = "http://localhost:8091/" + ep;
|
|
17
|
-
if (ep.toLowerCase().startsWith("http"))
|
|
18
|
-
url = ep;
|
|
19
|
-
let resp = null;
|
|
20
|
-
console.log("open url: ", url);
|
|
21
|
-
try {
|
|
22
|
-
if (m === "get")
|
|
23
|
-
resp = await axios_1.default.get(url, { headers: headers });
|
|
24
|
-
if (m === "post")
|
|
25
|
-
resp = await axios_1.default.post(url, data, { headers: headers });
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
console.log("ERROR! cant open url", e);
|
|
29
|
-
}
|
|
30
|
-
console.log(resp);
|
|
31
5
|
};
|
|
32
6
|
exports.LocalRequest = LocalRequest;
|
|
@@ -33,7 +33,7 @@ const DefaultErrors_1 = __importStar(require("../structures/DefaultErrors"));
|
|
|
33
33
|
const Validator_1 = require("../helper/Validator");
|
|
34
34
|
const UID_1 = require("../helper/UID");
|
|
35
35
|
const crypto_1 = __importDefault(require("crypto"));
|
|
36
|
-
const
|
|
36
|
+
const __1 = require("../..");
|
|
37
37
|
exports.REQ_EXTERNAL_CALL = new badmfck_signal_1.Req(undefined, "REQ_EXTERNAL_CALL");
|
|
38
38
|
exports.REQ_XT = new badmfck_signal_1.Req(undefined, "REQ_XT");
|
|
39
39
|
exports.REQ_XT.listener = async ({ id, req }) => {
|
|
@@ -89,33 +89,21 @@ class ExternalService extends BaseService_1.BaseService {
|
|
|
89
89
|
const encrypted = this.encrypt({ requestName, requestData });
|
|
90
90
|
if (this.options.url.endsWith("/"))
|
|
91
91
|
this.options.url = this.options.url.substring(0, this.options.url.length - 1);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"Accept": "application/json"
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
const data = resp.data;
|
|
101
|
-
if (typeof data === "object" && "error" in data)
|
|
102
|
-
return data.error;
|
|
103
|
-
if (typeof data === "object" && "data" in data)
|
|
104
|
-
return data.data;
|
|
105
|
-
return data;
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
const err = e;
|
|
109
|
-
if (err.response && err.response.data)
|
|
110
|
-
console.log("ExternalService.requestExternalCall", err.response.data);
|
|
111
|
-
else
|
|
112
|
-
console.error("ExternalService.requestExternalCall", err.cause);
|
|
113
|
-
if (err.response) {
|
|
114
|
-
if (err.response.data && typeof err.response.data === "object" && "error" in err.response.data)
|
|
115
|
-
return err.response.data.error;
|
|
92
|
+
const resp = await __1.Http.post(this.options.url + "/external/call/" + this.options.id, encrypted.data, {
|
|
93
|
+
headers: {
|
|
94
|
+
"authorization": encrypted.authorization,
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
"Accept": "application/json"
|
|
116
97
|
}
|
|
117
|
-
|
|
98
|
+
});
|
|
99
|
+
if (!resp.ok) {
|
|
100
|
+
return { ...DefaultErrors_1.default.BAD_REQUEST, details: resp.details, stack: resp.error };
|
|
118
101
|
}
|
|
102
|
+
const json = await resp.data.json();
|
|
103
|
+
if (typeof json === "object" && "error" in json)
|
|
104
|
+
return json.error;
|
|
105
|
+
if (typeof json === "object" && "data" in json)
|
|
106
|
+
return json.data;
|
|
119
107
|
}
|
|
120
108
|
async onExternalCall(req) {
|
|
121
109
|
const data = this.decrypt(req);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MicroserviceClient = void 0;
|
|
4
|
+
const BaseService_1 = require("../BaseService");
|
|
5
|
+
class MicroserviceClient extends BaseService_1.BaseService {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("MicroserviceClient");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.MicroserviceClient = MicroserviceClient;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MicroserviceHost = void 0;
|
|
4
|
+
const BaseService_1 = require("../BaseService");
|
|
5
|
+
class MicroserviceHost extends BaseService_1.BaseService {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("MicroserviceHost");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.MicroserviceHost = MicroserviceHost;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Dispatcher, Agent } from "undici";
|
|
2
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
3
|
+
export type THttpOk<T> = {
|
|
4
|
+
ok: true;
|
|
5
|
+
status: number;
|
|
6
|
+
data: T;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
export type THttpErr<E = any> = {
|
|
10
|
+
ok: false;
|
|
11
|
+
status?: number;
|
|
12
|
+
error: E;
|
|
13
|
+
details?: any;
|
|
14
|
+
};
|
|
15
|
+
export type THttpResult<T, E = any> = THttpOk<T> | THttpErr<E>;
|
|
16
|
+
export type TRetryPolicy = {
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
maxAttempts?: number;
|
|
19
|
+
baseDelayMs?: number;
|
|
20
|
+
maxDelayMs?: number;
|
|
21
|
+
retryOnStatuses?: number[];
|
|
22
|
+
retryOnNetworkErrors?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type THttpConfig = {
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
dispatcher?: Dispatcher;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
retry?: TRetryPolicy;
|
|
30
|
+
onRequest?: (ctx: {
|
|
31
|
+
method: HttpMethod;
|
|
32
|
+
url: string;
|
|
33
|
+
attempt: number;
|
|
34
|
+
}) => void;
|
|
35
|
+
onResponse?: (ctx: {
|
|
36
|
+
method: HttpMethod;
|
|
37
|
+
url: string;
|
|
38
|
+
attempt: number;
|
|
39
|
+
status: number;
|
|
40
|
+
ms: number;
|
|
41
|
+
}) => void;
|
|
42
|
+
onError?: (ctx: {
|
|
43
|
+
method: HttpMethod;
|
|
44
|
+
url: string;
|
|
45
|
+
attempt: number;
|
|
46
|
+
ms: number;
|
|
47
|
+
err: unknown;
|
|
48
|
+
}) => void;
|
|
49
|
+
};
|
|
50
|
+
export type TRequestOptions = {
|
|
51
|
+
method: HttpMethod;
|
|
52
|
+
url: string;
|
|
53
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
54
|
+
headers?: Record<string, string>;
|
|
55
|
+
body?: any;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
dispatcher?: Dispatcher;
|
|
58
|
+
responseType?: "json" | "text" | "raw";
|
|
59
|
+
throwOnJsonParseError?: boolean;
|
|
60
|
+
retry?: TRetryPolicy;
|
|
61
|
+
allowBody?: boolean;
|
|
62
|
+
};
|
|
63
|
+
export declare class Http {
|
|
64
|
+
private static _inited;
|
|
65
|
+
private static config;
|
|
66
|
+
private static _sharedAgent;
|
|
67
|
+
private static _dcEnabled;
|
|
68
|
+
private static _dcConnectedChannel;
|
|
69
|
+
private static _dcListener;
|
|
70
|
+
private static _activeConnectionsTotal;
|
|
71
|
+
private static _activeConnectionsByHost;
|
|
72
|
+
private static _countedSockets;
|
|
73
|
+
private static _socketHostKey;
|
|
74
|
+
private static ensureInit;
|
|
75
|
+
static configure(partial: Partial<THttpConfig>): void;
|
|
76
|
+
static createAgent(opts?: ConstructorParameters<typeof Agent>[0]): Agent;
|
|
77
|
+
static closeSharedAgent(): void;
|
|
78
|
+
static basicAuth(username: string, password: string): string;
|
|
79
|
+
static bearer(token: string): string;
|
|
80
|
+
static enableConnectionCounterByHost(): void;
|
|
81
|
+
static disableConnectionCounterByHost(options?: {
|
|
82
|
+
reset?: boolean;
|
|
83
|
+
}): void;
|
|
84
|
+
private static _makeHostKey;
|
|
85
|
+
static getActiveConnectionsTotal(): number;
|
|
86
|
+
static getActiveConnectionsByHost(): Map<string, number>;
|
|
87
|
+
static getActiveConnectionsSnapshot(): {
|
|
88
|
+
total: number;
|
|
89
|
+
byHost: Record<string, number>;
|
|
90
|
+
};
|
|
91
|
+
static request<T = any, E = any>(opts: TRequestOptions): Promise<THttpResult<T, E>>;
|
|
92
|
+
static get<T = any, E = any>(url: string, opts?: Omit<TRequestOptions, "method" | "url">): Promise<THttpResult<T, E>>;
|
|
93
|
+
static post<T = any, E = any>(url: string, body?: any, opts?: Omit<TRequestOptions, "method" | "url" | "body">): Promise<THttpResult<T, E>>;
|
|
94
|
+
static put<T = any, E = any>(url: string, body?: any, opts?: Omit<TRequestOptions, "method" | "url" | "body">): Promise<THttpResult<T, E>>;
|
|
95
|
+
static patch<T = any, E = any>(url: string, body?: any, opts?: Omit<TRequestOptions, "method" | "url" | "body">): Promise<THttpResult<T, E>>;
|
|
96
|
+
static delete<T = any, E = any>(url: string, opts?: Omit<TRequestOptions, "method" | "url">): Promise<THttpResult<T, E>>;
|
|
97
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Http = void 0;
|
|
7
|
+
const undici_1 = require("undici");
|
|
8
|
+
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
}
|
|
12
|
+
function clamp(n, min, max) {
|
|
13
|
+
return Math.max(min, Math.min(max, n));
|
|
14
|
+
}
|
|
15
|
+
function buildQuery(query) {
|
|
16
|
+
if (!query)
|
|
17
|
+
return "";
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
for (const [k, v] of Object.entries(query)) {
|
|
20
|
+
if (v === null || v === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
params.set(k, String(v));
|
|
23
|
+
}
|
|
24
|
+
const s = params.toString();
|
|
25
|
+
return s ? `?${s}` : "";
|
|
26
|
+
}
|
|
27
|
+
function mergeHeaders(a, b) {
|
|
28
|
+
return { ...(a || {}), ...(b || {}) };
|
|
29
|
+
}
|
|
30
|
+
function normalizeHeaders(headers) {
|
|
31
|
+
const out = {};
|
|
32
|
+
if (!headers || typeof headers !== "object")
|
|
33
|
+
return out;
|
|
34
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
35
|
+
if (Array.isArray(v))
|
|
36
|
+
out[k.toLowerCase()] = v.join(", ");
|
|
37
|
+
else if (v !== undefined && v !== null)
|
|
38
|
+
out[k.toLowerCase()] = String(v);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function isRetryableStatus(status, retryOnStatuses) {
|
|
43
|
+
return retryOnStatuses.includes(status);
|
|
44
|
+
}
|
|
45
|
+
function isLikelyNetworkError(err) {
|
|
46
|
+
if (!err || typeof err !== "object")
|
|
47
|
+
return false;
|
|
48
|
+
const anyErr = err;
|
|
49
|
+
if (anyErr?.name === "AbortError")
|
|
50
|
+
return true;
|
|
51
|
+
const code = anyErr?.code;
|
|
52
|
+
const networkCodes = new Set(["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "EAI_AGAIN", "ENOTFOUND", "EPIPE"]);
|
|
53
|
+
if (code && networkCodes.has(code))
|
|
54
|
+
return true;
|
|
55
|
+
if (err instanceof undici_1.errors.ConnectTimeoutError)
|
|
56
|
+
return true;
|
|
57
|
+
if (err instanceof undici_1.errors.HeadersTimeoutError)
|
|
58
|
+
return true;
|
|
59
|
+
if (err instanceof undici_1.errors.BodyTimeoutError)
|
|
60
|
+
return true;
|
|
61
|
+
if (err instanceof undici_1.errors.SocketError)
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
class Http {
|
|
66
|
+
static _inited = false;
|
|
67
|
+
static config = {
|
|
68
|
+
baseUrl: "",
|
|
69
|
+
timeoutMs: 5000,
|
|
70
|
+
headers: {
|
|
71
|
+
accept: "application/json",
|
|
72
|
+
},
|
|
73
|
+
retry: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
maxAttempts: 2,
|
|
76
|
+
baseDelayMs: 200,
|
|
77
|
+
maxDelayMs: 1500,
|
|
78
|
+
retryOnStatuses: [429, 502, 503, 504],
|
|
79
|
+
retryOnNetworkErrors: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
static _sharedAgent = null;
|
|
83
|
+
static _dcEnabled = false;
|
|
84
|
+
static _dcConnectedChannel = null;
|
|
85
|
+
static _dcListener = null;
|
|
86
|
+
static _activeConnectionsTotal = 0;
|
|
87
|
+
static _activeConnectionsByHost = new Map();
|
|
88
|
+
static _countedSockets = new WeakSet();
|
|
89
|
+
static _socketHostKey = new WeakMap();
|
|
90
|
+
static ensureInit() {
|
|
91
|
+
if (this._inited)
|
|
92
|
+
return;
|
|
93
|
+
this._inited = true;
|
|
94
|
+
this.enableConnectionCounterByHost();
|
|
95
|
+
if (!this.config.dispatcher) {
|
|
96
|
+
this._sharedAgent = this.createAgent({
|
|
97
|
+
connections: 100,
|
|
98
|
+
keepAliveTimeout: 60_000,
|
|
99
|
+
keepAliveMaxTimeout: 60_000,
|
|
100
|
+
});
|
|
101
|
+
this.config.dispatcher = this._sharedAgent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
static configure(partial) {
|
|
105
|
+
this.config = {
|
|
106
|
+
...this.config,
|
|
107
|
+
...partial,
|
|
108
|
+
headers: mergeHeaders(this.config.headers, partial.headers),
|
|
109
|
+
retry: { ...(this.config.retry || {}), ...(partial.retry || {}) },
|
|
110
|
+
};
|
|
111
|
+
if (partial.dispatcher && this._sharedAgent) {
|
|
112
|
+
try {
|
|
113
|
+
this._sharedAgent.close();
|
|
114
|
+
}
|
|
115
|
+
catch { }
|
|
116
|
+
this._sharedAgent = null;
|
|
117
|
+
}
|
|
118
|
+
this.ensureInit();
|
|
119
|
+
}
|
|
120
|
+
static createAgent(opts) {
|
|
121
|
+
return new undici_1.Agent({
|
|
122
|
+
keepAliveTimeout: 60_000,
|
|
123
|
+
keepAliveMaxTimeout: 60_000,
|
|
124
|
+
...opts,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
static closeSharedAgent() {
|
|
128
|
+
if (!this._sharedAgent)
|
|
129
|
+
return;
|
|
130
|
+
try {
|
|
131
|
+
this._sharedAgent.close();
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
this._sharedAgent = null;
|
|
135
|
+
if (this.config.dispatcher === this._sharedAgent) {
|
|
136
|
+
this.config.dispatcher = undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
static basicAuth(username, password) {
|
|
140
|
+
const token = Buffer.from(`${username}:${password}`).toString("base64");
|
|
141
|
+
return `Basic ${token}`;
|
|
142
|
+
}
|
|
143
|
+
static bearer(token) {
|
|
144
|
+
return `Bearer ${token}`;
|
|
145
|
+
}
|
|
146
|
+
static enableConnectionCounterByHost() {
|
|
147
|
+
if (this._dcEnabled)
|
|
148
|
+
return;
|
|
149
|
+
this._dcEnabled = true;
|
|
150
|
+
const ch = node_diagnostics_channel_1.default.channel("undici:client:connected");
|
|
151
|
+
const listener = (message) => {
|
|
152
|
+
const msg = message;
|
|
153
|
+
const socket = msg?.socket;
|
|
154
|
+
if (!socket)
|
|
155
|
+
return;
|
|
156
|
+
if (this._countedSockets.has(socket))
|
|
157
|
+
return;
|
|
158
|
+
this._countedSockets.add(socket);
|
|
159
|
+
const key = this._makeHostKey(msg?.connectParams, socket);
|
|
160
|
+
this._socketHostKey.set(socket, key);
|
|
161
|
+
this._activeConnectionsTotal += 1;
|
|
162
|
+
this._activeConnectionsByHost.set(key, (this._activeConnectionsByHost.get(key) ?? 0) + 1);
|
|
163
|
+
const onClose = () => {
|
|
164
|
+
this._activeConnectionsTotal = Math.max(0, this._activeConnectionsTotal - 1);
|
|
165
|
+
const k = this._socketHostKey.get(socket) ?? "unknown://unknown:0";
|
|
166
|
+
const prev = this._activeConnectionsByHost.get(k) ?? 0;
|
|
167
|
+
const next = Math.max(0, prev - 1);
|
|
168
|
+
if (next === 0)
|
|
169
|
+
this._activeConnectionsByHost.delete(k);
|
|
170
|
+
else
|
|
171
|
+
this._activeConnectionsByHost.set(k, next);
|
|
172
|
+
socket.off("close", onClose);
|
|
173
|
+
};
|
|
174
|
+
socket.on("close", onClose);
|
|
175
|
+
};
|
|
176
|
+
ch.subscribe(listener);
|
|
177
|
+
this._dcConnectedChannel = ch;
|
|
178
|
+
this._dcListener = listener;
|
|
179
|
+
}
|
|
180
|
+
static disableConnectionCounterByHost(options) {
|
|
181
|
+
if (!this._dcEnabled)
|
|
182
|
+
return;
|
|
183
|
+
const ch = this._dcConnectedChannel;
|
|
184
|
+
const listener = this._dcListener;
|
|
185
|
+
if (ch && listener) {
|
|
186
|
+
ch.unsubscribe(listener);
|
|
187
|
+
}
|
|
188
|
+
this._dcConnectedChannel = null;
|
|
189
|
+
this._dcListener = null;
|
|
190
|
+
this._dcEnabled = false;
|
|
191
|
+
if (options?.reset) {
|
|
192
|
+
this._activeConnectionsTotal = 0;
|
|
193
|
+
this._activeConnectionsByHost.clear();
|
|
194
|
+
this._countedSockets = new WeakSet();
|
|
195
|
+
this._socketHostKey = new WeakMap();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
static _makeHostKey(connectParams, socket) {
|
|
199
|
+
const protocolRaw = connectParams?.protocol || "unknown:";
|
|
200
|
+
const protocol = protocolRaw.replace(/\/+$/g, "");
|
|
201
|
+
const hostname = connectParams?.hostname || connectParams?.host;
|
|
202
|
+
let port = connectParams?.port !== undefined && connectParams?.port !== null ? String(connectParams.port) : undefined;
|
|
203
|
+
if (!port) {
|
|
204
|
+
if (protocol === "https:")
|
|
205
|
+
port = "443";
|
|
206
|
+
else if (protocol === "http:")
|
|
207
|
+
port = "80";
|
|
208
|
+
else
|
|
209
|
+
port = "?";
|
|
210
|
+
}
|
|
211
|
+
if (hostname)
|
|
212
|
+
return `${protocol}//${hostname}:${port}`;
|
|
213
|
+
const addr = socket?.remoteAddress ?? "unknown";
|
|
214
|
+
const p = socket?.remotePort ?? "?";
|
|
215
|
+
return `${protocol}//${addr}:${p}`;
|
|
216
|
+
}
|
|
217
|
+
static getActiveConnectionsTotal() {
|
|
218
|
+
return this._activeConnectionsTotal;
|
|
219
|
+
}
|
|
220
|
+
static getActiveConnectionsByHost() {
|
|
221
|
+
return new Map(this._activeConnectionsByHost);
|
|
222
|
+
}
|
|
223
|
+
static getActiveConnectionsSnapshot() {
|
|
224
|
+
return {
|
|
225
|
+
total: this._activeConnectionsTotal,
|
|
226
|
+
byHost: Object.fromEntries(this._activeConnectionsByHost.entries()),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
static async request(opts) {
|
|
230
|
+
this.ensureInit();
|
|
231
|
+
const cfg = this.config;
|
|
232
|
+
const retryCfg = {
|
|
233
|
+
enabled: opts.retry?.enabled ?? cfg.retry?.enabled ?? true,
|
|
234
|
+
maxAttempts: opts.retry?.maxAttempts ?? cfg.retry?.maxAttempts ?? 2,
|
|
235
|
+
baseDelayMs: opts.retry?.baseDelayMs ?? cfg.retry?.baseDelayMs ?? 200,
|
|
236
|
+
maxDelayMs: opts.retry?.maxDelayMs ?? cfg.retry?.maxDelayMs ?? 1500,
|
|
237
|
+
retryOnStatuses: opts.retry?.retryOnStatuses ?? cfg.retry?.retryOnStatuses ?? [429, 502, 503, 504],
|
|
238
|
+
retryOnNetworkErrors: opts.retry?.retryOnNetworkErrors ?? cfg.retry?.retryOnNetworkErrors ?? true,
|
|
239
|
+
};
|
|
240
|
+
const timeoutMs = opts.timeoutMs ?? cfg.timeoutMs ?? 5000;
|
|
241
|
+
const dispatcher = opts.dispatcher ?? cfg.dispatcher;
|
|
242
|
+
const baseUrl = cfg.baseUrl || "";
|
|
243
|
+
const isAbsolute = /^https?:\/\//i.test(opts.url);
|
|
244
|
+
const query = buildQuery(opts.query);
|
|
245
|
+
const url = (isAbsolute ? opts.url : baseUrl + opts.url) + query;
|
|
246
|
+
const headers = mergeHeaders(cfg.headers, opts.headers);
|
|
247
|
+
let body = undefined;
|
|
248
|
+
const hasBody = opts.body !== undefined && opts.body !== null;
|
|
249
|
+
const allowBody = opts.allowBody ?? (opts.method !== "GET" && opts.method !== "DELETE");
|
|
250
|
+
if (hasBody && allowBody) {
|
|
251
|
+
const ct = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
|
|
252
|
+
if (!ct)
|
|
253
|
+
headers["content-type"] = "application/json";
|
|
254
|
+
const ct2 = (headers["content-type"] || headers["Content-Type"] || "").toLowerCase();
|
|
255
|
+
if (ct2.includes("application/json") && typeof opts.body !== "string" && !(opts.body instanceof Buffer)) {
|
|
256
|
+
body = JSON.stringify(opts.body);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
body = opts.body;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const responseType = opts.responseType ?? "json";
|
|
263
|
+
const throwOnJsonParseError = opts.throwOnJsonParseError ?? false;
|
|
264
|
+
for (let attempt = 1; attempt <= retryCfg.maxAttempts; attempt++) {
|
|
265
|
+
const started = Date.now();
|
|
266
|
+
cfg.onRequest?.({ method: opts.method, url, attempt });
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
269
|
+
try {
|
|
270
|
+
const res = await (0, undici_1.request)(url, {
|
|
271
|
+
method: opts.method,
|
|
272
|
+
headers,
|
|
273
|
+
body,
|
|
274
|
+
signal: controller.signal,
|
|
275
|
+
dispatcher,
|
|
276
|
+
});
|
|
277
|
+
const ms = Date.now() - started;
|
|
278
|
+
const status = res.statusCode;
|
|
279
|
+
cfg.onResponse?.({ method: opts.method, url, attempt, status, ms });
|
|
280
|
+
const hdrs = normalizeHeaders(res.headers);
|
|
281
|
+
let parsed = undefined;
|
|
282
|
+
let rawText;
|
|
283
|
+
if (responseType === "raw") {
|
|
284
|
+
await res.body.dump();
|
|
285
|
+
parsed = undefined;
|
|
286
|
+
}
|
|
287
|
+
else if (responseType === "text") {
|
|
288
|
+
rawText = await res.body.text();
|
|
289
|
+
parsed = rawText;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
rawText = await res.body.text();
|
|
293
|
+
if (rawText.length === 0) {
|
|
294
|
+
parsed = null;
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(rawText);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
if (throwOnJsonParseError)
|
|
302
|
+
throw e;
|
|
303
|
+
parsed = rawText;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (status >= 200 && status < 300) {
|
|
308
|
+
return { ok: true, status, data: parsed, headers: hdrs };
|
|
309
|
+
}
|
|
310
|
+
const retryable = retryCfg.enabled &&
|
|
311
|
+
isRetryableStatus(status, retryCfg.retryOnStatuses) &&
|
|
312
|
+
attempt < retryCfg.maxAttempts;
|
|
313
|
+
if (retryable) {
|
|
314
|
+
const backoff = clamp(retryCfg.baseDelayMs * Math.pow(2, attempt - 1), retryCfg.baseDelayMs, retryCfg.maxDelayMs);
|
|
315
|
+
await sleep(backoff);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
return { ok: false, status, error: parsed, details: { headers: hdrs } };
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const ms = Date.now() - started;
|
|
322
|
+
cfg.onError?.({ method: opts.method, url, attempt, ms, err });
|
|
323
|
+
const retryable = retryCfg.enabled &&
|
|
324
|
+
retryCfg.retryOnNetworkErrors &&
|
|
325
|
+
isLikelyNetworkError(err) &&
|
|
326
|
+
attempt < retryCfg.maxAttempts;
|
|
327
|
+
if (retryable) {
|
|
328
|
+
const backoff = clamp(retryCfg.baseDelayMs * Math.pow(2, attempt - 1), retryCfg.baseDelayMs, retryCfg.maxDelayMs);
|
|
329
|
+
await sleep(backoff);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
return { ok: false, error: err, details: { network: true } };
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
clearTimeout(t);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { ok: false, error: "UNKNOWN" };
|
|
339
|
+
}
|
|
340
|
+
static get(url, opts) {
|
|
341
|
+
return this.request({ ...(opts || {}), method: "GET", url });
|
|
342
|
+
}
|
|
343
|
+
static post(url, body, opts) {
|
|
344
|
+
return this.request({ ...(opts || {}), method: "POST", url, body });
|
|
345
|
+
}
|
|
346
|
+
static put(url, body, opts) {
|
|
347
|
+
return this.request({ ...(opts || {}), method: "PUT", url, body });
|
|
348
|
+
}
|
|
349
|
+
static patch(url, body, opts) {
|
|
350
|
+
return this.request({ ...(opts || {}), method: "PATCH", url, body });
|
|
351
|
+
}
|
|
352
|
+
static delete(url, opts) {
|
|
353
|
+
return this.request({ ...(opts || {}), method: "DELETE", url });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
exports.Http = Http;
|
|
@@ -13,7 +13,7 @@ const DefaultErrors_1 = __importDefault(require("../structures/DefaultErrors"));
|
|
|
13
13
|
const MonitorService_1 = require("../MonitorService");
|
|
14
14
|
const fs_1 = __importDefault(require("fs"));
|
|
15
15
|
const path_1 = __importDefault(require("path"));
|
|
16
|
-
const
|
|
16
|
+
const Http_1 = require("../http/Http");
|
|
17
17
|
exports.S_MONITOR_REGISTRATE_ACTION = new badmfck_signal_1.Signal();
|
|
18
18
|
const logMap = {
|
|
19
19
|
ALL: 10,
|
|
@@ -54,43 +54,11 @@ class Monitor extends BaseEndpoint_1.BaseEndpoint {
|
|
|
54
54
|
this.lastTimeCacheChecked = +new Date();
|
|
55
55
|
let response = null;
|
|
56
56
|
try {
|
|
57
|
-
response = await
|
|
57
|
+
response = await Http_1.Http.get("https://igorbloom.com/dist/monitor.html");
|
|
58
58
|
}
|
|
59
59
|
catch (e) {
|
|
60
60
|
(0, LogService_1.logError)("Error loading monitor.html from remote server", e);
|
|
61
61
|
}
|
|
62
|
-
if (response && response.status === 200 && response.data && response.data.length > 0) {
|
|
63
|
-
if (typeof response.data !== "string")
|
|
64
|
-
(0, LogService_1.logError)("Monitor HTML file is not a string", response.data);
|
|
65
|
-
else {
|
|
66
|
-
const hash = crypto_1.default.createHash("sha256").update(response.data).digest("hex");
|
|
67
|
-
const oldHash = crypto_1.default.createHash("sha256").update(this.indexHtmlFile).digest("hex");
|
|
68
|
-
if (hash !== oldHash) {
|
|
69
|
-
if (response.data.length < 1000) {
|
|
70
|
-
(0, LogService_1.logError)("Monitor HTML file is too small, probably not loaded correctly", response.data);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
if (response.data.substring(0, 24).toLowerCase().indexOf("<!doctype html>") === -1) {
|
|
74
|
-
(0, LogService_1.logError)("Monitor HTML file is not a valid HTML file, <!doctype html> tag not found");
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
try {
|
|
78
|
-
fs_1.default.writeFileSync(path_1.default.resolve(__dirname, "index.html"), response.data, "utf-8");
|
|
79
|
-
this.fileParsed = false;
|
|
80
|
-
this.indexHtmlFile = response.data;
|
|
81
|
-
(0, LogService_1.logInfo)("Monitor HTML file updated from remote server");
|
|
82
|
-
}
|
|
83
|
-
catch (e) {
|
|
84
|
-
(0, LogService_1.logError)("Error writing monitor HTML file to local disk", e);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
(0, LogService_1.logInfo)("Remote monitor HTML file has the same hash");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
62
|
}
|
|
95
63
|
return this.indexHtmlFile;
|
|
96
64
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ import { YYYYMMDDHH } from "./apiServer/helper/YYYYMMDDHH";
|
|
|
13
13
|
import { TimeframeService } from "./apiServer/TimeframeService";
|
|
14
14
|
import { JSONStableStringify } from "./apiServer/helper/JSONStableStringify";
|
|
15
15
|
import { ZipUtils } from "./apiServer/helper/ZipUtils";
|
|
16
|
-
|
|
16
|
+
import { Http } from "./apiServer/http/Http";
|
|
17
|
+
export { Http, ZipUtils, UID, YYYYMMDDHH, JSONStableStringify, APIService, Initializer, LocalRequest, ValidationModel, MysqlService, TimeframeService, Validator, LogService, DataProvider, ErrorUtils, ExternalService, DBService, S_MONITOR_REGISTRATE_ACTION };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.S_MONITOR_REGISTRATE_ACTION = exports.DBService = exports.ExternalService = exports.ErrorUtils = exports.DataProvider = exports.LogService = exports.Validator = exports.TimeframeService = exports.MysqlService = exports.LocalRequest = exports.Initializer = exports.APIService = exports.JSONStableStringify = exports.YYYYMMDDHH = exports.UID = exports.ZipUtils = void 0;
|
|
3
|
+
exports.S_MONITOR_REGISTRATE_ACTION = exports.DBService = exports.ExternalService = exports.ErrorUtils = exports.DataProvider = exports.LogService = exports.Validator = exports.TimeframeService = exports.MysqlService = exports.LocalRequest = exports.Initializer = exports.APIService = exports.JSONStableStringify = exports.YYYYMMDDHH = exports.UID = exports.ZipUtils = exports.Http = void 0;
|
|
4
4
|
const APIService_1 = require("./apiServer/APIService");
|
|
5
5
|
Object.defineProperty(exports, "APIService", { enumerable: true, get: function () { return APIService_1.APIService; } });
|
|
6
6
|
Object.defineProperty(exports, "Initializer", { enumerable: true, get: function () { return APIService_1.Initializer; } });
|
|
@@ -32,3 +32,5 @@ const JSONStableStringify_1 = require("./apiServer/helper/JSONStableStringify");
|
|
|
32
32
|
Object.defineProperty(exports, "JSONStableStringify", { enumerable: true, get: function () { return JSONStableStringify_1.JSONStableStringify; } });
|
|
33
33
|
const ZipUtils_1 = require("./apiServer/helper/ZipUtils");
|
|
34
34
|
Object.defineProperty(exports, "ZipUtils", { enumerable: true, get: function () { return ZipUtils_1.ZipUtils; } });
|
|
35
|
+
const Http_1 = require("./apiServer/http/Http");
|
|
36
|
+
Object.defineProperty(exports, "Http", { enumerable: true, get: function () { return Http_1.Http; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badmfck-api-server",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.94",
|
|
4
4
|
"description": "Simple API http server based on express",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,15 +19,16 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@types/express-fileupload": "^1.5.0",
|
|
21
21
|
"@types/mysql": "^2.15.21",
|
|
22
|
+
"@types/node": "^25.0.3",
|
|
22
23
|
"@types/pg": "^8.15.5",
|
|
23
24
|
"@types/ws": "^8.5.9",
|
|
24
|
-
"axios": "^1.10.0",
|
|
25
25
|
"badmfck-signal": "^1.4.9",
|
|
26
26
|
"cors": "^2.8.5",
|
|
27
27
|
"express": "^4.21.2",
|
|
28
28
|
"express-fileupload": "^1.5.2",
|
|
29
29
|
"mysql2": "^3.14.2",
|
|
30
30
|
"pg": "^8.16.3",
|
|
31
|
+
"undici": "^7.18.0",
|
|
31
32
|
"ws": "^8.18.3"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|