Repository navigation
Expand file tree
/
Copy pathclient.js
More file actions
143 lines (136 loc) · 6.14 KB
/
Copy pathclient.js
File metadata and controls
143 lines (136 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"use strict";
const http = require('http');
const https = require('https');
const fs = require('fs');
const { URL } = require('url');
const isNode = (typeof process !== 'undefined') && process.versions?.node && (typeof Buffer !== 'undefined');
/**
* HTTPSockClient
*
* Client that connects to an `httpsock` server GET endpoint and receives a framed stream of messages.
*
* @param {Object} [options]
* @param {string} [options.server='https://sub.domain.tld:port/path'] - URL of the httpsock server GET endpoint.
* @param {string|Buffer} [options.cert='./certs/chain.pem'] - Optional certificate chain for HTTPS servers.
* @param {function} [options.callback=(response) => console.log('←', response)] - Callback for each received message (may receive Buffer, string, or parsed JSON).
* @param {function} [options.close=() => console.log('↓ stream closed')] - Called when the stream ends.
* @param {function} [options.error=(err) => console.error('↓', err)] - Called on errors.
* @param {string|Object} [options.auth] - Optional basic auth string 'user:pass' or { username, password } used for Authorization header.
*
* Methods:
* - stream(): open the streaming connection
* - stop(): stop the streaming connection
*/
class HTTPSockClient {
constructor(options = {
server: 'https://sub.domain.tld:port/path',
cert: './certs/chain.pem',
callback: ((response) => { console.log('←', response) }),
close: (() => { console.log('↓ stream closed') }),
error: ((error) => { console.error('↓', error) })
}) {
this.server = options.server || 'http://localhost:1234/';
if (options.cert && ((isNode ? Buffer.isBuffer(options.cert) : false) || ((typeof options.cert === 'string') && options.cert.includes('BEGIN CERTIFICATE')))) {
this.cert = options.cert;
} else if (isNode) {
try {
this.cert = fs.readFileSync((typeof options.cert === 'string') ? options.cert : './certs/chain.pem');
} catch (e) {
this.cert = undefined;
};
} else {
this.cert = undefined;
};
this.auth = options.auth;
this.callback = options.callback || (response => console.log('←', response));
this.close = options.close || (() => console.log('↓ stream closed'));
this.error = options.error || (error => console.error('↓', error));
this.err = this.err.bind(this);
this.request = null;
this.connected = false;
};
async err(error) {
this.connected = false;
if (error === 'ended' || (error && (error.code === 'ECONNRESET'))) {
await this.close();
} else {
await this.error(error);
};
};
stream() {
console.log('↑ connecting...');
const url = new URL(this.server);
this.request = ((url.protocol === 'https:') ? https : http).request(
{
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'GET',
headers: this.auth ? {
Authorization: ((typeof this.auth === 'string') && (this.auth.indexOf(':') === -1)) ? ('Basic ' + this.auth) : ('Basic ' + Buffer.from((typeof this.auth === 'string') ? this.auth : (this.auth.username + ':' + this.auth.password)).toString('base64'))
} : undefined,
ca: this.cert
},
res => {
var buffer = Buffer.alloc(0);
const feed = async (chunk) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
while (true) {
const crlfIdx = buffer.indexOf('\r\n');
if (crlfIdx === -1) break;
const sizeLine = buffer.subarray(0, crlfIdx).toString().trim();
const size = parseInt(sizeLine, 16);
if (Number.isNaN(size)) {
this.err(new Error('Invalid httpsock size: ' + sizeLine));
return;
};
const frameTotal = crlfIdx + 2 + size + 2;
if (buffer.length < frameTotal) break;
const payloadStart = crlfIdx + 2;
const payload = buffer.subarray(payloadStart, payloadStart + size);
buffer = buffer.subarray(frameTotal);
var out = payload;
try {
const isText = (buf) => {
for (var i = 0; i < buf.length; i++) {
const b = buf[i];
if ((b === 9) || (b === 10) || (b === 13)) continue;
if ((b >= 32) && (b <= 126)) continue;
return false;
};
return true;
};
if (Buffer.isBuffer(payload) && isText(payload)) {
const s = payload.toString();
try {
out = JSON.parse(s);
} catch (e) {
out = s;
};
};
} catch (convErr) {
out = payload;
};
try {
await this.callback(out);
} catch (err) {
this.err(err);
};
};
};
res.on('data', feed);
res.on('end', async () => {
this.connected = false;
await this.close();
});
}
);
this.request.on('error', this.err);
this.request.end();
};
stop() {
if (this.request) this.request.destroy('ended');
this.connected = false;
};
};
module.exports = HTTPSockClient;