@basenjs/base-http 0.0.1

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.
@@ -0,0 +1,431 @@
1
+ 'use strict';
2
+
3
+ var https = require('node:https');
4
+ var base = require('@basenjs/base');
5
+ var http = require('node:http');
6
+ var qs = require('qs');
7
+ var multiparty = require('multiparty');
8
+
9
+ class BaseHttp extends base.BaseObject {
10
+ constructor() {
11
+ super();
12
+ this.logLevel = 'none';
13
+ }
14
+ static processResponse(response) {
15
+ return new Promise((resolve, reject) => {
16
+ // if(res.statusCode == '302') { // redirect
17
+ // if('location' in res.headers) {
18
+ // _.statusCode = 302;
19
+ // _.setHeader('Location', res.headers['location']);
20
+ // }
21
+ // }
22
+ });
23
+ }
24
+ static buildOptions(options) {
25
+ let headers = options.headers || {}, r = {
26
+ hostname: options.hostname || 'localhost',
27
+ path: options.path || '/',
28
+ port: options.port || (options.protocol === 'https' ? 443 : 80),
29
+ method: options.method || 'GET',
30
+ headers: {
31
+ 'Content-Type': headers['Content-Type'] || 'application/json',
32
+ 'Connection': headers['Connection'] || 'keep-alive',
33
+ 'Accept': headers['Accept'] || '*/*',
34
+ 'Accept-Encoding': headers['Accept-Encoding'] || 'deflate, br, zstd',
35
+ ...headers
36
+ }
37
+ };
38
+ return r;
39
+ }
40
+ static head(options) {
41
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'HEAD' });
42
+ }
43
+ static options(options) {
44
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'OPTIONS' });
45
+ }
46
+ static get(options) {
47
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'GET' });
48
+ }
49
+ static post(options, body) {
50
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
51
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'POST' }, body);
52
+ }
53
+ static put(options, body) {
54
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
55
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'PUT' }, body);
56
+ }
57
+ static patch(options, body) {
58
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
59
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'PATCH' }, body);
60
+ }
61
+ static delete(options) {
62
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'DELETE' });
63
+ }
64
+ static async request(options, body = null, callbacks = { request: () => { }, data: () => { }, end: () => { } }) {
65
+ var buffer = Buffer.alloc(0);
66
+ const req = https.request(options, (res) => {
67
+ console.log('<p>https://' + options.hostname + options.path + '</p>');
68
+ console.log(`<p>STATUS: ${res.statusCode}</p>`);
69
+ console.log(`<p>HEADERS: ${JSON.stringify(res.headers)}</p>`);
70
+ res.setEncoding('utf8');
71
+ callbacks.request(res);
72
+ res.on('data', (chunk) => {
73
+ // this.log.debug(`<p>BODY: ${chunk}</p>`);
74
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
75
+ callbacks.data(buffer);
76
+ });
77
+ res.on('end', () => {
78
+ // this.log.debug('No more data in response.');
79
+ callbacks.end(buffer);
80
+ return buffer;
81
+ });
82
+ });
83
+ req.on('error', (e) => {
84
+ console.log(`problem with request: ${e.message}`);
85
+ console.log(e.stack);
86
+ throw (e);
87
+ });
88
+ if (body && (options.method?.toLowerCase() in ['post', 'put', 'patch'])) {
89
+ req.write(body);
90
+ }
91
+ req.end();
92
+ return req;
93
+ }
94
+ }
95
+
96
+ class BaseHttpBody extends base.BaseObject {
97
+ _readable;
98
+ _chunks = [];
99
+ _dataWasRead = false;
100
+ _ended = false;
101
+ constructor(readable) {
102
+ super();
103
+ this._readable = readable;
104
+ }
105
+ get readable() {
106
+ return this._readable;
107
+ }
108
+ get dataWasRead() {
109
+ return this._dataWasRead;
110
+ }
111
+ async readAsBuffer() {
112
+ var _ = this;
113
+ return new Promise((resolve, reject) => {
114
+ var applyResolve = function () {
115
+ let readBody = '';
116
+ if (!_.dataWasRead && ('body' in _.readable) && _.readable.body) {
117
+ // body can be an object
118
+ readBody = typeof _.readable.body == 'object' ? JSON.stringify(_.readable.body) : _.readable.body + '';
119
+ _.log.info('BodyReader::readBodyAsBuffer _readable.body: ' + readBody);
120
+ _._chunks.push(Buffer.from(readBody));
121
+ }
122
+ resolve(Buffer.concat(_._chunks));
123
+ };
124
+ this._readable.on("data", (chunk) => {
125
+ _._dataWasRead = true;
126
+ _.log.debug('BodyReader event (data)');
127
+ _._chunks.push(chunk);
128
+ });
129
+ this._readable.on('close', () => {
130
+ _.log.debug('BodyReader event (close):');
131
+ if (!_._ended) {
132
+ applyResolve();
133
+ }
134
+ });
135
+ this._readable.on("end", () => {
136
+ _.log.debug('BodyReader event (end):');
137
+ _._ended = true;
138
+ applyResolve();
139
+ });
140
+ this._readable.on("error", (error) => {
141
+ _.log.error('BodyReader event (error): ' + error.message);
142
+ throw new Error(error.message);
143
+ });
144
+ });
145
+ }
146
+ ;
147
+ }
148
+
149
+ class BaseMultipartData extends base.BaseObject {
150
+ _form = new multiparty.Form();
151
+ _bodyRaw = Buffer.alloc(0);
152
+ fields = [];
153
+ files = [];
154
+ get form() {
155
+ return this._form;
156
+ }
157
+ get bodyRaw() {
158
+ return this._bodyRaw;
159
+ }
160
+ constructor() {
161
+ super();
162
+ }
163
+ parse(request) {
164
+ return this._parse(request);
165
+ }
166
+ parseRawText(text = '') {
167
+ if (text.length < 1) {
168
+ return;
169
+ }
170
+ }
171
+ _parse(request) {
172
+ var _ = this;
173
+ return new Promise((resolve, reject) => {
174
+ let contentType = request.headers['content-type'] || '', chunks = new Array();
175
+ // Errors may be emitted
176
+ // Note that if you are listening to 'part' events, the same error may be
177
+ // emitted from the `form` and the `part`.
178
+ _.form.on('error', function (err) {
179
+ _.log.debug('initMultipart: Error parsing form: ' + err.stack);
180
+ });
181
+ _.form.on('pipe', function (readable) {
182
+ _.log.debug('initMultipart: pipe ');
183
+ if (readable) {
184
+ readable.on('data', (chunk) => {
185
+ _.log.debug('initMultipart: piped readable: data ');
186
+ _.log.debug('initMultipart: piped readable:' + chunk);
187
+ chunks.push(chunk);
188
+ });
189
+ }
190
+ });
191
+ _.form.on('data', function (chunk) {
192
+ _.log.debug('initMultipart: data ');
193
+ });
194
+ // Parts are emitted when parsing the form
195
+ _.form.on('part', function (part) {
196
+ // You *must* act on the part by reading it
197
+ // NOTE: if you want to ignore it, just call "part.resume()"
198
+ if (part.filename === undefined) {
199
+ // filename is not defined when this is a field and not a file
200
+ _.log.debug('initMultipart: got field named ' + part.name);
201
+ // ignore field's content
202
+ part.resume();
203
+ }
204
+ if (part.filename !== undefined) {
205
+ // filename is defined when this is a file
206
+ // count++;
207
+ _.log.debug('initMultipart: got file named ' + part.name);
208
+ // ignore file's content here
209
+ part.resume();
210
+ }
211
+ part.on('error', function (err) {
212
+ // decide what to do
213
+ });
214
+ });
215
+ // Close emitted after form parsed
216
+ _.form.on('close', function () {
217
+ _.log.debug('initMultipart (requestIn): Upload completed!');
218
+ // res.setHeader('text/plain');
219
+ // res.end('Received ' + count + ' files');
220
+ _._bodyRaw = Buffer.concat(chunks);
221
+ resolve(_.bodyRaw);
222
+ });
223
+ if (contentType.match(/multipart/i) != null) {
224
+ // Parse req
225
+ _.form.parse(request);
226
+ }
227
+ else {
228
+ resolve('');
229
+ }
230
+ });
231
+ }
232
+ }
233
+
234
+ class BaseHttpProxy extends base.BaseObject {
235
+ requestIn;
236
+ responseOut;
237
+ forwardComplete = false;
238
+ _multipartData = new BaseMultipartData();
239
+ _requestInBodyReader;
240
+ ;
241
+ _responseInBodyReader;
242
+ forwardWaitCount = 0;
243
+ httpRequest = http.request({});
244
+ httpRequestInRawChunks = [];
245
+ httpRequestInRaw = Buffer.alloc(0);
246
+ httpResponseRawChunks = [];
247
+ httpResponseRaw = Buffer.alloc(0);
248
+ httpResponseRawChunksSize = 0;
249
+ WRITEABLE = {
250
+ 'post': 'POST',
251
+ 'patch': 'PATCH',
252
+ 'put': 'PUT'
253
+ };
254
+ HOST = 'ws.anything-2493814af352';
255
+ PATH_BASE = '/_p';
256
+ MAX_WAIT_COUNT = 30; // in seconds
257
+ constructor(requestIn, responseOut) {
258
+ super();
259
+ this.requestIn = requestIn;
260
+ this.responseOut = responseOut;
261
+ this._requestInBodyReader = new BaseHttpBody(this.requestIn);
262
+ this._responseInBodyReader = new BaseHttpBody(this.responseOut);
263
+ }
264
+ async forwardRequest() {
265
+ this.log.info('forwardRequest()');
266
+ this._initForwardRequest();
267
+ // this.httpRequest.end(); // move this to areas after writing
268
+ while (!this.forwardComplete && this.forwardWaitCount < this.MAX_WAIT_COUNT) {
269
+ await this.sleep(1000);
270
+ this.forwardWaitCount++;
271
+ }
272
+ }
273
+ sleep(millis) {
274
+ return new Promise(resolve => setTimeout(resolve, millis));
275
+ }
276
+ initEvents() {
277
+ var _ = this;
278
+ this.httpRequest.on('socket', function (socket) {
279
+ _.log.debug('socket (httpRequest out)');
280
+ socket.on('data', (chunk) => {
281
+ _.httpResponseRawChunksSize += chunk.length;
282
+ _.httpResponseRawChunks.push(Buffer.from(chunk));
283
+ _.log.debug('socket data chunk size (httpRequest out): ' + _.httpResponseRawChunksSize);
284
+ _.log.debug('socket data chunk (httpRequest out): ' + chunk);
285
+ });
286
+ socket.on('end', () => {
287
+ _.log.debug('socket end (httpRequest out)');
288
+ });
289
+ socket.on('close', () => {
290
+ _.log.debug('socket close (httpRequest out)');
291
+ _.log.debug('socket close chunk size (httpRequest out): ' + _.httpResponseRawChunksSize);
292
+ });
293
+ socket.resume();
294
+ });
295
+ }
296
+ _initHeaders() {
297
+ let contentType = this.requestIn.headers['content-type'];
298
+ this.requestIn.headers['host'] = this.HOST;
299
+ if (this.requestIn?.method?.toLowerCase() == 'post' && contentType?.match(/multipart/i) == null) {
300
+ this.log.info('setting content type for post');
301
+ this.requestIn.headers['content-type'] = 'application/x-www-form-urlencoded';
302
+ delete this.requestIn.headers['connection'];
303
+ // delete this.requestIn.headers['keep-alive'];
304
+ }
305
+ }
306
+ _initForwardRequest() {
307
+ var _ = this;
308
+ this.log.info('_initForwardRequest()');
309
+ this._initHeaders();
310
+ // DEBUG
311
+ this.log.debug('_initForwardRequest method (requestIn):' + this.requestIn.method);
312
+ // this.log.debug('_initForwardRequest body (requestIn):' + JSON.stringify(this.requestIn.body));
313
+ this.log.debug('_initForwardRequest headers (requestIn):' + JSON.stringify(this.requestIn.headers));
314
+ // this.log.debug('_initForwardRequest path (requestIn):' + JSON.stringify(this.requestIn.path));
315
+ this._requestInBodyReader = new BaseHttpBody(this.requestIn);
316
+ this._requestInBodyReader.readAsBuffer().then((data) => {
317
+ this.log.debug('_requestInBodyReader finished reading');
318
+ _.httpRequestInRaw.write(data.toString());
319
+ _._forwardRequest();
320
+ });
321
+ }
322
+ _forwardRequest() {
323
+ let url = new URL(this.requestIn.url || ''), query = qs.stringify(url.searchParams), path = url.pathname?.replace(this.PATH_BASE, '') + (query != '' ? '?' + query : '');
324
+ this.log.debug('_forwardRequest path (requestIn): ' + qs.stringify(url.pathname));
325
+ this.log.debug('_forwardRequest query (requestIn): ' + qs.stringify(url.searchParams));
326
+ const options = {
327
+ protocol: 'http:',
328
+ host: this.HOST,
329
+ port: 14599,
330
+ path: path,
331
+ method: this.requestIn.method,
332
+ headers: this.requestIn.headers,
333
+ };
334
+ this.log.debug(JSON.stringify(options));
335
+ var _ = this;
336
+ this.httpRequest = http.request(options);
337
+ this.httpRequest.on('response', (responseIn) => {
338
+ _.log.info('httpRequest response:');
339
+ _._responseInBodyReader = new BaseHttpBody(responseIn);
340
+ _._responseInBodyReader.readAsBuffer().then((data) => {
341
+ _.log.debug('_responseInBodyReader finished reading');
342
+ let headers = {}, dataBuffer = Buffer.alloc(0), contentType = responseIn.headers['Content-Type'] || '';
343
+ contentType = typeof contentType == 'string' ? contentType : contentType.join('; ');
344
+ _.log.debug('-- end event (callback)');
345
+ _.log.debug('-- chunks collected length: ' + Buffer.byteLength(data));
346
+ _.log.debug('-- chunks: ' + data.toString());
347
+ let fIsolateData = function (httpResponseRaw) {
348
+ let EOH = '\r\n\r\n';
349
+ return (httpResponseRaw.slice(httpResponseRaw.indexOf(EOH) + EOH.length));
350
+ };
351
+ dataBuffer = fIsolateData(Buffer.concat(_.httpResponseRawChunks));
352
+ if (contentType.length > 0) {
353
+ headers['Content-Type'] = responseIn.headers['content-type'];
354
+ if (contentType.match(/multipart|image/i) != null) {
355
+ headers['Content-Length'] = Buffer.byteLength(dataBuffer);
356
+ }
357
+ // if(responseIn.headers['content-type'] == 'image/png') {
358
+ // headers['Content-Length'] = Buffer.byteLength(dataBuffer);
359
+ // }
360
+ }
361
+ else {
362
+ headers['Content-Type'] = 'application/json';
363
+ }
364
+ _.log.debug('sending response with headers: ' + JSON.stringify(headers));
365
+ if (contentType.match(/json|text|html|xml/gim) != null) {
366
+ _.log.debug('sending text');
367
+ _.log.debug('sending text headers (responseOut):' + JSON.stringify(headers));
368
+ _.responseOut.write(data);
369
+ }
370
+ else {
371
+ _.log.debug('sending binary');
372
+ _.log.debug('sending binary headers (responseOut):' + JSON.stringify(headers));
373
+ // Logger.debug(data);
374
+ _.responseOut.writeHead(responseIn.statusCode, headers);
375
+ _.responseOut.write(dataBuffer);
376
+ }
377
+ // _.log.debug('buffer:');
378
+ // _.log.debug(data);
379
+ // _.log.debug('-- end buffer');
380
+ // this writes everything out and is not contexualized by content type above
381
+ // responseOut.send(data);
382
+ _.forwardComplete = true;
383
+ });
384
+ });
385
+ this.initEvents();
386
+ let method = this.requestIn?.method?.toLowerCase() || '';
387
+ if (method in this.WRITEABLE) {
388
+ this.log.info('writing writeable: ' + Buffer.byteLength(this.httpRequestInRaw));
389
+ this._writeWritable(this.httpRequestInRaw).then((bodyWritten) => {
390
+ this.httpRequest.end();
391
+ });
392
+ }
393
+ else {
394
+ this.httpRequest.end(this.httpRequestInRaw);
395
+ }
396
+ // this._multipartData.parse(this.requestIn).then(bodyRaw => {
397
+ // this.writePost();
398
+ // });
399
+ }
400
+ _writeWritable(bodyBuffer) {
401
+ var _ = this;
402
+ return new Promise((resolve, reject) => {
403
+ let contentType = _.requestIn.headers['Content-Type'] || '', bodyObject = null, dataPostParts = [], method = this.requestIn?.method?.toLowerCase() || '';
404
+ contentType = typeof contentType == 'string' ? contentType : contentType.join('; ');
405
+ if (!bodyBuffer || (!(method in _.WRITEABLE))) {
406
+ resolve([]);
407
+ }
408
+ if (method == 'post' && contentType.match(/multipart/i) == null) {
409
+ bodyObject = JSON.parse(bodyBuffer.toString());
410
+ _.log.debug('writing post:' + JSON.stringify(bodyObject));
411
+ _.log.debug('post data: ' + JSON.stringify(bodyObject));
412
+ for (let key in bodyObject) {
413
+ dataPostParts.push(key + '=' + encodeURIComponent(bodyObject[key]));
414
+ }
415
+ _.log.debug('writing post data: ' + dataPostParts.join('&'));
416
+ _.httpRequest.write(dataPostParts.join('&'));
417
+ resolve(dataPostParts);
418
+ }
419
+ else {
420
+ _.log.debug('writing data: ' + JSON.stringify(bodyBuffer));
421
+ _.httpRequest.write(bodyBuffer);
422
+ resolve(bodyBuffer);
423
+ }
424
+ });
425
+ }
426
+ }
427
+
428
+ exports.BaseHttp = BaseHttp;
429
+ exports.BaseHttpBody = BaseHttpBody;
430
+ exports.BaseHttpProxy = BaseHttpProxy;
431
+ exports.BaseMultipartData = BaseMultipartData;