@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,116 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import { Url } from 'node:url';
4
+
5
+ import { BaseObject } from '@basenjs/base';
6
+
7
+ export class BaseHttp extends BaseObject {
8
+ constructor() {
9
+ super();
10
+ this.logLevel = 'none';
11
+ }
12
+
13
+ public static processResponse(response: http.IncomingMessage): Promise<Buffer> {
14
+ return new Promise<Buffer>((resolve, reject) => {
15
+ const chunks: Buffer[] = [];
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
+
25
+ public static buildOptions(options: http.RequestOptions | any | Url): http.RequestOptions {
26
+ let headers = options.headers || {},
27
+ r : http.RequestOptions = {
28
+ hostname: options.hostname || 'localhost',
29
+ path: options.path || '/',
30
+ port: options.port || (options.protocol==='https' ? 443 : 80),
31
+ method: options.method || 'GET',
32
+ headers: {
33
+ 'Content-Type': headers['Content-Type'] || 'application/json',
34
+ 'Connection': headers['Connection'] || 'keep-alive',
35
+ 'Accept': headers['Accept'] || '*/*',
36
+ 'Accept-Encoding': headers['Accept-Encoding'] || 'deflate, br, zstd',
37
+ ...headers
38
+ }
39
+ };
40
+
41
+ return r;
42
+ }
43
+
44
+ public static head(options: http.RequestOptions | any | Url): Promise<http.ClientRequest | undefined> {
45
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'HEAD' });
46
+ }
47
+
48
+ public static options(options: http.RequestOptions | any | Url): Promise<http.ClientRequest | undefined> {
49
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'OPTIONS' });
50
+ }
51
+
52
+ public static get(options: http.RequestOptions | any | Url): Promise<http.ClientRequest | undefined> {
53
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'GET' });
54
+ }
55
+
56
+ public static post(options: http.RequestOptions | any | Url, body: any): Promise<http.ClientRequest | undefined> {
57
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
58
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'POST' }, body);
59
+ }
60
+
61
+ public static put(options: http.RequestOptions | any | Url, body: any): Promise<http.ClientRequest | undefined> {
62
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
63
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'PUT' }, body);
64
+ }
65
+
66
+ public static patch(options: http.RequestOptions | any | Url, body: any): Promise<http.ClientRequest | undefined> {
67
+ options.headers['Content-Length'] = body ? Buffer.byteLength(body) : 0;
68
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'PATCH' }, body);
69
+ }
70
+
71
+ public static delete(options: http.RequestOptions | any | Url): Promise<http.ClientRequest | undefined> {
72
+ return BaseHttp.request({ ...BaseHttp.buildOptions(options), method: 'DELETE' });
73
+ }
74
+
75
+ public static async request(options: http.RequestOptions | any | Url, body: any = null, callbacks: {request: (response: http.IncomingMessage) => void, data: (buffer: Buffer) => void, end: (buffer: Buffer) => void} = {request: () => {}, data: () => {}, end: () => {}}): Promise<http.ClientRequest | undefined> {
76
+ var _ = this,
77
+ buffer: Buffer = Buffer.alloc(0);
78
+
79
+ const req = https.request(options, (res) => {
80
+ console.log('<p>https://' + options.hostname + options.path + '</p>');
81
+ console.log(`<p>STATUS: ${res.statusCode}</p>`);
82
+ console.log(`<p>HEADERS: ${JSON.stringify(res.headers)}</p>`);
83
+
84
+ res.setEncoding('utf8');
85
+
86
+ callbacks.request(res);
87
+
88
+ res.on('data', (chunk) => {
89
+ // this.log.debug(`<p>BODY: ${chunk}</p>`);
90
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
91
+ callbacks.data(buffer);
92
+ });
93
+
94
+ res.on('end', () => {
95
+ // this.log.debug('No more data in response.');
96
+ callbacks.end(buffer);
97
+ return buffer;
98
+ });
99
+ });
100
+
101
+ req.on('error', (e) => {
102
+ console.log(`problem with request: ${e.message}`);
103
+ console.log(e.stack);
104
+
105
+ throw(e);
106
+ });
107
+
108
+ if (body && (options.method?.toLowerCase() in ['post', 'put', 'patch'])) {
109
+ req.write(body);
110
+ }
111
+
112
+ req.end();
113
+
114
+ return req;
115
+ }
116
+ }
@@ -0,0 +1,67 @@
1
+ import http from 'node:http';
2
+
3
+ import { BaseObject } from '@basenjs/base';
4
+
5
+ export class BaseHttpBody extends BaseObject {
6
+ _readable: http.ClientRequest | http.IncomingMessage;
7
+ _chunks: Buffer[] = [];
8
+ _dataWasRead: boolean = false;
9
+ _ended: boolean = false;
10
+
11
+ constructor(readable: http.ClientRequest | http.IncomingMessage | any) {
12
+ super();
13
+ this._readable = readable;
14
+ }
15
+
16
+ public get readable(): http.ClientRequest | http.IncomingMessage {
17
+ return this._readable;
18
+ }
19
+
20
+ public get dataWasRead(): boolean {
21
+ return this._dataWasRead;
22
+ }
23
+
24
+ async readAsBuffer(): Promise<Buffer> {
25
+ var _ = this;
26
+
27
+ return new Promise<Buffer>((resolve, reject) => {
28
+
29
+ var applyResolve = function() {
30
+ let readBody = '';
31
+ if(!_.dataWasRead && ('body' in _.readable) && _.readable.body) {
32
+ // body can be an object
33
+
34
+ readBody = typeof _.readable.body == 'object' ? JSON.stringify(_.readable.body) : _.readable.body + '';
35
+ _.log.info('BodyReader::readBodyAsBuffer _readable.body: ' + readBody);
36
+ _._chunks.push(Buffer.from(readBody));
37
+ }
38
+ resolve(Buffer.concat(_._chunks));
39
+ };
40
+
41
+ this._readable.on("data", (chunk: Buffer) => {
42
+ _._dataWasRead = true;
43
+ _.log.debug('BodyReader event (data)');
44
+ _._chunks.push(chunk);
45
+ });
46
+
47
+ this._readable.on('close', () => {
48
+ _.log.debug('BodyReader event (close):');
49
+ if(!_._ended) {
50
+ applyResolve();
51
+ }
52
+ });
53
+
54
+ this._readable.on("end", () => {
55
+ _.log.debug('BodyReader event (end):');
56
+ _._ended = true;
57
+ applyResolve();
58
+ });
59
+
60
+ this._readable.on("error", (error) => {
61
+ _.log.error('BodyReader event (error): ' + error.message);
62
+ throw new Error(error.message);
63
+ });
64
+
65
+ });
66
+ };
67
+ }
@@ -0,0 +1,285 @@
1
+ import http from 'node:http';
2
+ import { Url } from 'node:url';
3
+
4
+ import qs from 'qs';
5
+
6
+ import { BaseObject } from '@basenjs/base';
7
+
8
+ import { BaseHttpBody } from './BaseHttpBody';
9
+ import { BaseMultipartData } from './BaseMultipartData';
10
+
11
+ export class BaseHttpProxy extends BaseObject {
12
+ requestIn: http.IncomingMessage;
13
+ responseOut: http.ServerResponse;
14
+ forwardComplete = false;
15
+ _multipartData = new BaseMultipartData();
16
+
17
+ _requestInBodyReader: BaseHttpBody;;
18
+ _responseInBodyReader: BaseHttpBody;
19
+
20
+ forwardWaitCount = 0;
21
+
22
+ httpRequest: http.ClientRequest = http.request({});
23
+
24
+ httpRequestInRawChunks: Buffer[] = [];
25
+ httpRequestInRaw = Buffer.alloc(0);
26
+
27
+ httpResponseRawChunks: Buffer[] = [];
28
+ httpResponseRaw = Buffer.alloc(0);
29
+
30
+ httpResponseRawChunksSize = 0;
31
+
32
+ WRITEABLE = {
33
+ 'post':'POST',
34
+ 'patch':'PATCH',
35
+ 'put':'PUT'
36
+ };
37
+
38
+ HOST = 'ws.anything-2493814af352';
39
+ PATH_BASE = '/_p';
40
+ MAX_WAIT_COUNT = 30; // in seconds
41
+
42
+ constructor(requestIn: any, responseOut: any) {
43
+ super();
44
+ this.requestIn = requestIn;
45
+ this.responseOut = responseOut;
46
+
47
+ this._requestInBodyReader = new BaseHttpBody(this.requestIn);
48
+ this._responseInBodyReader = new BaseHttpBody(this.responseOut);
49
+ }
50
+
51
+ async forwardRequest() {
52
+
53
+ this.log.info('forwardRequest()');
54
+
55
+ this._initForwardRequest();
56
+
57
+ // this.httpRequest.end(); // move this to areas after writing
58
+
59
+ while(!this.forwardComplete && this.forwardWaitCount < this.MAX_WAIT_COUNT) {
60
+ await this.sleep(1000);
61
+ this.forwardWaitCount++;
62
+ }
63
+ }
64
+
65
+ sleep(millis: number) {
66
+ return new Promise(resolve => setTimeout(resolve, millis));
67
+ }
68
+
69
+ initEvents() {
70
+ var _ = this;
71
+
72
+ this.httpRequest.on('socket', function (socket: any) {
73
+ _.log.debug('socket (httpRequest out)');
74
+
75
+ socket.on('data', (chunk: string) => {
76
+ _.httpResponseRawChunksSize += chunk.length;
77
+ _.httpResponseRawChunks.push(Buffer.from(chunk));
78
+ _.log.debug('socket data chunk size (httpRequest out): ' + _.httpResponseRawChunksSize);
79
+ _.log.debug('socket data chunk (httpRequest out): ' + chunk);
80
+ });
81
+
82
+ socket.on('end', () => {
83
+ _.log.debug('socket end (httpRequest out)');
84
+ });
85
+
86
+ socket.on('close', () => {
87
+ _.log.debug('socket close (httpRequest out)');
88
+ _.log.debug('socket close chunk size (httpRequest out): ' + _.httpResponseRawChunksSize);
89
+ });
90
+
91
+ socket.resume();
92
+ });
93
+ }
94
+
95
+
96
+ _initHeaders() {
97
+ let contentType = this.requestIn.headers['content-type'];
98
+
99
+ this.requestIn.headers['host'] = this.HOST;
100
+
101
+ if(this.requestIn?.method?.toLowerCase() == 'post' && contentType?.match(/multipart/i) == null) {
102
+ this.log.info('setting content type for post');
103
+ this.requestIn.headers['content-type'] = 'application/x-www-form-urlencoded';
104
+ delete this.requestIn.headers['connection'];
105
+ // delete this.requestIn.headers['keep-alive'];
106
+ }
107
+ }
108
+
109
+
110
+ _initForwardRequest() {
111
+ var _= this;
112
+
113
+ this.log.info('_initForwardRequest()');
114
+
115
+ this._initHeaders();
116
+
117
+ // DEBUG
118
+ this.log.debug('_initForwardRequest method (requestIn):' + this.requestIn.method);
119
+ // this.log.debug('_initForwardRequest body (requestIn):' + JSON.stringify(this.requestIn.body));
120
+ this.log.debug('_initForwardRequest headers (requestIn):' + JSON.stringify(this.requestIn.headers));
121
+ // this.log.debug('_initForwardRequest path (requestIn):' + JSON.stringify(this.requestIn.path));
122
+
123
+ this._requestInBodyReader = new BaseHttpBody(this.requestIn);
124
+
125
+ this._requestInBodyReader.readAsBuffer().then((data: Buffer) => {
126
+ this.log.debug('_requestInBodyReader finished reading');
127
+ _.httpRequestInRaw.write(data.toString());
128
+ _._forwardRequest();
129
+ });
130
+ }
131
+
132
+ _forwardRequest() {
133
+ let url: URL = new URL(this.requestIn.url || ''),
134
+ query = qs.stringify(url.searchParams),
135
+ path = url.pathname?.replace(this.PATH_BASE, '') + (query != '' ? '?' + query : '');
136
+
137
+ this.log.debug('_forwardRequest path (requestIn): ' + qs.stringify(url.pathname));
138
+ this.log.debug('_forwardRequest query (requestIn): ' + qs.stringify(url.searchParams));
139
+
140
+ const options = {
141
+ protocol: 'http:',
142
+ host: this.HOST,
143
+ port: 14599,
144
+ path: path,
145
+ method: this.requestIn.method,
146
+ headers: this.requestIn.headers,
147
+ };
148
+
149
+ this.log.debug(JSON.stringify(options));
150
+
151
+ var _ = this;
152
+
153
+ this.httpRequest = http.request(options);
154
+
155
+ this.httpRequest.on('response', (responseIn) =>
156
+ {
157
+ _.log.info('httpRequest response:');
158
+
159
+ _._responseInBodyReader = new BaseHttpBody(responseIn);
160
+
161
+ _._responseInBodyReader.readAsBuffer().then((data) => {
162
+ _.log.debug('_responseInBodyReader finished reading');
163
+
164
+ let headers: http.OutgoingHttpHeaders = {},
165
+ dataBuffer = Buffer.alloc(0),
166
+ contentType: string | string[] = responseIn.headers['Content-Type'] || '';
167
+
168
+ contentType = typeof contentType == 'string' ? contentType : contentType.join('; ');
169
+
170
+ _.log.debug('-- end event (callback)');
171
+ _.log.debug('-- chunks collected length: ' + Buffer.byteLength(data));
172
+ _.log.debug('-- chunks: ' + data.toString());
173
+
174
+ let fIsolateData = function(httpResponseRaw: Buffer) {
175
+ let EOH = '\r\n\r\n',
176
+ eohPos = -1;
177
+ return(httpResponseRaw.slice(httpResponseRaw.indexOf(EOH) + EOH.length));
178
+ };
179
+
180
+ dataBuffer = fIsolateData(Buffer.concat(_.httpResponseRawChunks));
181
+
182
+ if(contentType.length > 0) {
183
+ headers['Content-Type'] = responseIn.headers['content-type'];
184
+
185
+ if(contentType.match(/multipart|image/i) != null) {
186
+ headers['Content-Length'] = Buffer.byteLength(dataBuffer);
187
+ }
188
+
189
+ // if(responseIn.headers['content-type'] == 'image/png') {
190
+ // headers['Content-Length'] = Buffer.byteLength(dataBuffer);
191
+ // }
192
+ }
193
+ else {
194
+ headers['Content-Type'] = 'application/json';
195
+ }
196
+
197
+ _.log.debug('sending response with headers: ' + JSON.stringify(headers));
198
+
199
+ if(contentType.match(/json|text|html|xml/gim) != null) {
200
+ _.log.debug('sending text');
201
+ _.log.debug('sending text headers (responseOut):' + JSON.stringify(headers));
202
+
203
+ _.responseOut.write(data);
204
+ }
205
+ else {
206
+ _.log.debug('sending binary');
207
+ _.log.debug('sending binary headers (responseOut):' + JSON.stringify(headers));
208
+ // Logger.debug(data);
209
+
210
+ _.responseOut.writeHead(responseIn.statusCode!, headers);
211
+ _.responseOut.write(dataBuffer);
212
+ }
213
+
214
+ // _.log.debug('buffer:');
215
+ // _.log.debug(data);
216
+ // _.log.debug('-- end buffer');
217
+
218
+ // this writes everything out and is not contexualized by content type above
219
+ // responseOut.send(data);
220
+
221
+ _.forwardComplete = true;
222
+ });
223
+ });
224
+
225
+ this.initEvents();
226
+
227
+ let method = this.requestIn?.method?.toLowerCase() || '';
228
+
229
+ if(method in this.WRITEABLE) {
230
+ this.log.info('writing writeable: ' + Buffer.byteLength(this.httpRequestInRaw));
231
+ this._writeWritable(this.httpRequestInRaw).then((bodyWritten) => {
232
+ this.httpRequest.end();
233
+ });
234
+ }
235
+ else {
236
+ this.httpRequest.end(this.httpRequestInRaw);
237
+ }
238
+
239
+ // this._multipartData.parse(this.requestIn).then(bodyRaw => {
240
+ // this.writePost();
241
+ // });
242
+ }
243
+
244
+
245
+ _writeWritable(bodyBuffer: Buffer) {
246
+
247
+ var _ = this;
248
+
249
+ return new Promise((resolve, reject) => {
250
+
251
+ let contentType = _.requestIn.headers['Content-Type'] || '',
252
+ bodyObject = null,
253
+ dataPostParts = [],
254
+ method = this.requestIn?.method?.toLowerCase() || '';
255
+
256
+ contentType = typeof contentType == 'string' ? contentType : contentType.join('; ');
257
+
258
+ if(!bodyBuffer || (!(method in _.WRITEABLE))) {
259
+ resolve([]);
260
+ }
261
+
262
+ if(method == 'post' && contentType.match(/multipart/i) == null) {
263
+ bodyObject = JSON.parse(bodyBuffer.toString());
264
+
265
+ _.log.debug('writing post:' + JSON.stringify(bodyObject));
266
+ _.log.debug('post data: ' + JSON.stringify(bodyObject));
267
+ for(let key in bodyObject) {
268
+ dataPostParts.push(key + '=' + encodeURIComponent(bodyObject[key]));
269
+ }
270
+
271
+ _.log.debug('writing post data: ' + dataPostParts.join('&'));
272
+
273
+ _.httpRequest.write(dataPostParts.join('&'));
274
+
275
+ resolve(dataPostParts);
276
+ }
277
+ else {
278
+ _.log.debug('writing data: ' + JSON.stringify(bodyBuffer));
279
+ _.httpRequest.write(bodyBuffer);
280
+
281
+ resolve(bodyBuffer);
282
+ }
283
+ });
284
+ }
285
+ }
@@ -0,0 +1,106 @@
1
+ import multiparty from 'multiparty';
2
+ import { BaseObject } from '@basenjs/base';
3
+
4
+ export class BaseMultipartData extends BaseObject {
5
+ _form = new multiparty.Form();
6
+ _bodyRaw = Buffer.alloc(0);
7
+
8
+ fields: any[] = [];
9
+ files: any[] = [];
10
+
11
+ public get form(): multiparty.Form {
12
+ return this._form;
13
+ }
14
+
15
+ public get bodyRaw(): Buffer {
16
+ return this._bodyRaw;
17
+ }
18
+
19
+ constructor() {
20
+ super();
21
+ }
22
+
23
+ parse(request: any) {
24
+ return this._parse(request);
25
+ }
26
+
27
+ parseRawText(text = '') {
28
+ if(text.length < 1) {
29
+ return;
30
+ }
31
+ }
32
+
33
+ private _parse(request: any) {
34
+ var _ = this;
35
+
36
+ return new Promise((resolve, reject) => {
37
+
38
+ let contentType = request.headers['content-type'] || '',
39
+ chunks: Buffer[] = new Array<Buffer>();
40
+
41
+ // Errors may be emitted
42
+ // Note that if you are listening to 'part' events, the same error may be
43
+ // emitted from the `form` and the `part`.
44
+ _.form.on('error', function(err: any) {
45
+ _.log.debug('initMultipart: Error parsing form: ' + err.stack);
46
+ });
47
+
48
+ _.form.on('pipe', function(readable: any) {
49
+ _.log.debug('initMultipart: pipe ');
50
+ if(readable) {
51
+ readable.on('data', (chunk: any) => {
52
+ _.log.debug('initMultipart: piped readable: data ');
53
+ _.log.debug('initMultipart: piped readable:' + chunk);
54
+ chunks.push(chunk);
55
+ });
56
+ }
57
+ });
58
+
59
+ _.form.on('data', function(chunk: any) {
60
+ _.log.debug('initMultipart: data ');
61
+ });
62
+
63
+ // Parts are emitted when parsing the form
64
+ _.form.on('part', function(part: any) {
65
+ // You *must* act on the part by reading it
66
+ // NOTE: if you want to ignore it, just call "part.resume()"
67
+
68
+ if (part.filename === undefined) {
69
+ // filename is not defined when this is a field and not a file
70
+ _.log.debug('initMultipart: got field named ' + part.name);
71
+ // ignore field's content
72
+ part.resume();
73
+ }
74
+
75
+ if (part.filename !== undefined) {
76
+ // filename is defined when this is a file
77
+ // count++;
78
+ _.log.debug('initMultipart: got file named ' + part.name);
79
+ // ignore file's content here
80
+ part.resume();
81
+ }
82
+
83
+ part.on('error', function(err: any) {
84
+ // decide what to do
85
+ });
86
+ });
87
+
88
+ // Close emitted after form parsed
89
+ _.form.on('close', function() {
90
+ _.log.debug('initMultipart (requestIn): Upload completed!');
91
+ // res.setHeader('text/plain');
92
+ // res.end('Received ' + count + ' files');
93
+ _._bodyRaw = Buffer.concat(chunks);
94
+ resolve(_.bodyRaw);
95
+ });
96
+
97
+ if(contentType.match(/multipart/i) != null) {
98
+ // Parse req
99
+ _.form.parse(request);
100
+ }
101
+ else {
102
+ resolve('');
103
+ }
104
+ });
105
+ }
106
+ }
@@ -0,0 +1,4 @@
1
+ export { BaseHttp } from './BaseHttp';
2
+ export { BaseHttpBody } from './BaseHttpBody';
3
+ export { BaseHttpProxy } from './BaseHttpProxy';
4
+ export { BaseMultipartData } from './BaseMultipartData';
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "emitDeclarationOnly": true,
6
+ "outDir": "dist/types"
7
+ }
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "esModuleInterop": true,
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleResolution": "bundler",
8
+ "rootDir": "src"
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }