@seriousme/opifex 2.3.0 → 2.4.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.
- package/client/client.ts +5 -0
- package/connNext/baseConnNext.ts +123 -0
- package/deno/tlsServer.test.ts +1 -1
- package/deno/wsServer.ts +11 -3
- package/dev_utils/WebStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamPair.ts +4 -9
- package/dev_utils/asyncStreamPairToNext.ts +45 -0
- package/dev_utils/asyncStreamToNext.ts +30 -137
- package/dev_utils/connNextPair.test.ts +2 -2
- package/dev_utils/generateCert.ts +297 -77
- package/dev_utils/mockConn.ts +37 -93
- package/dev_utils/mockServer.ts +1 -1
- package/dev_utils/webStreamPair.ts +3 -7
- package/dev_utils/webStreamPairToNext.ts +48 -0
- package/dev_utils/webStreamToNext.ts +28 -161
- package/dist/client/client.d.ts +4 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -0
- package/dist/client/client.js.map +1 -1
- package/dist/connNext/baseConnNext.d.ts +20 -0
- package/dist/connNext/baseConnNext.d.ts.map +1 -0
- package/dist/connNext/baseConnNext.js +95 -0
- package/dist/connNext/baseConnNext.js.map +1 -0
- package/dist/dev_utils/generateCert.d.ts +5 -4
- package/dist/dev_utils/generateCert.d.ts.map +1 -1
- package/dist/dev_utils/generateCert.js +230 -65
- package/dist/dev_utils/generateCert.js.map +1 -1
- package/dist/examples/mqtt+mqtts.test.js +1 -1
- package/dist/examples/mqtt+mqtts.test.js.map +1 -1
- package/dist/examples/mqtts.test.js +1 -1
- package/dist/examples/mqtts.test.js.map +1 -1
- package/dist/node/nodeSocketToNext.d.ts +10 -13
- package/dist/node/nodeSocketToNext.d.ts.map +1 -1
- package/dist/node/nodeSocketToNext.js +75 -131
- package/dist/node/nodeSocketToNext.js.map +1 -1
- package/dist/web/webSocketToNext.d.ts +18 -1
- package/dist/web/webSocketToNext.d.ts.map +1 -1
- package/dist/web/webSocketToNext.js +92 -127
- package/dist/web/webSocketToNext.js.map +1 -1
- package/dist/web/wsClient.d.ts.map +1 -1
- package/dist/web/wsClient.js +4 -3
- package/dist/web/wsClient.js.map +1 -1
- package/examples/mqtt+mqtts.test.ts +1 -1
- package/examples/mqtts.test.ts +1 -1
- package/node/nodeSocketToNext.ts +85 -148
- package/node/tlsServer.test.ts +1 -1
- package/package.json +2 -4
- package/web/webSocketToNext.ts +108 -145
- package/web/wsClient.ts +10 -3
- package/dev_utils/webStreamPair.test.ts +0 -171
|
@@ -1,95 +1,315 @@
|
|
|
1
|
-
import forge from "node-forge";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* Generates a minimal
|
|
5
|
-
*
|
|
2
|
+
* Generates a minimal TLS certificate bundle for localhost using Web Crypto API.
|
|
3
|
+
* Uses 0 external dependencies (pure JS ASN.1 DER encoding).
|
|
4
|
+
* Do NOT use this in a production setting
|
|
6
5
|
*/
|
|
7
|
-
export function generateLocalhostCerts(): {
|
|
6
|
+
export async function generateLocalhostCerts(): Promise<{
|
|
8
7
|
caCert: string;
|
|
9
8
|
key: string;
|
|
10
9
|
cert: string;
|
|
11
10
|
fullChain: string;
|
|
12
|
-
} {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Hardcoded CA Attributes
|
|
29
|
-
const caAttrs = [{ name: "commonName", value: "Local Dev Root CA" }];
|
|
30
|
-
caCert.setSubject(caAttrs);
|
|
31
|
-
caCert.setIssuer(caAttrs); // Self-signed: issuer equals subject
|
|
32
|
-
|
|
33
|
-
// Critical for CA: Define basic constraints to flag this certificate as a trusted issuer
|
|
34
|
-
caCert.setExtensions([
|
|
35
|
-
{ name: "basicConstraints", cA: true, critical: true },
|
|
36
|
-
{
|
|
37
|
-
name: "keyUsage",
|
|
38
|
-
keyCertSign: true,
|
|
39
|
-
digitalSignature: true,
|
|
40
|
-
critical: true,
|
|
41
|
-
},
|
|
11
|
+
}> {
|
|
12
|
+
const algorithm: RsaHashedKeyGenParams = {
|
|
13
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
14
|
+
hash: "SHA-256",
|
|
15
|
+
modulusLength: 2048,
|
|
16
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 1. Generate Keypairs using Web Crypto
|
|
20
|
+
const caKeys = await crypto.subtle.generateKey(algorithm, true, [
|
|
21
|
+
"sign",
|
|
22
|
+
"verify",
|
|
23
|
+
]);
|
|
24
|
+
const serverKeys = await crypto.subtle.generateKey(algorithm, true, [
|
|
25
|
+
"sign",
|
|
26
|
+
"verify",
|
|
42
27
|
]);
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// Create server certificate for localhost, valid for a year
|
|
48
|
-
const serverCert = forge.pki.createCertificate();
|
|
49
|
-
serverCert.publicKey = serverKeys.publicKey;
|
|
50
|
-
serverCert.serialNumber = "02"; // Unique serial number within this CA scope
|
|
51
|
-
serverCert.validity.notBefore = new Date();
|
|
52
|
-
serverCert.validity.notAfter = new Date();
|
|
53
|
-
serverCert.validity.notAfter.setFullYear(
|
|
54
|
-
serverCert.validity.notBefore.getFullYear() + 1,
|
|
55
|
-
);
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const nextYear = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
|
|
56
31
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
keyEncipherment: true,
|
|
69
|
-
critical: true,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: "subjectAltName",
|
|
73
|
-
altNames: [
|
|
74
|
-
{ type: 2, value: "localhost" }, // type 2 = DNS
|
|
75
|
-
{ type: 7, ip: "127.0.0.1" }, // type 7 = IPv4 loopback
|
|
76
|
-
{ type: 7, ip: "::1" }, // type 7 = IPv6 loopback
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
]);
|
|
32
|
+
// 2. Generate Root CA Certificate (Self-signed)
|
|
33
|
+
const caCertDer = await createX509Cert({
|
|
34
|
+
serialNumber: 1,
|
|
35
|
+
subjectCN: "Local Dev Root CA",
|
|
36
|
+
issuerCN: "Local Dev Root CA",
|
|
37
|
+
notBefore: now,
|
|
38
|
+
notAfter: nextYear,
|
|
39
|
+
publicKey: caKeys.publicKey,
|
|
40
|
+
signingPrivateKey: caKeys.privateKey,
|
|
41
|
+
isCA: true,
|
|
42
|
+
});
|
|
80
43
|
|
|
81
|
-
//
|
|
82
|
-
|
|
44
|
+
// 3. Generate Server Certificate (Signed by Root CA)
|
|
45
|
+
const serverCertDer = await createX509Cert({
|
|
46
|
+
serialNumber: 2,
|
|
47
|
+
subjectCN: "localhost",
|
|
48
|
+
issuerCN: "Local Dev Root CA",
|
|
49
|
+
notBefore: now,
|
|
50
|
+
notAfter: nextYear,
|
|
51
|
+
publicKey: serverKeys.publicKey,
|
|
52
|
+
signingPrivateKey: caKeys.privateKey,
|
|
53
|
+
isCA: false,
|
|
54
|
+
sans: [
|
|
55
|
+
{ type: "dns", value: "localhost" },
|
|
56
|
+
{ type: "ip", value: "127.0.0.1" },
|
|
57
|
+
{ type: "ip", value: "::1" },
|
|
58
|
+
],
|
|
59
|
+
});
|
|
83
60
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
61
|
+
// Export Server Private Key to PKCS#8 PEM
|
|
62
|
+
const exportedPrivateKey = await crypto.subtle.exportKey(
|
|
63
|
+
"pkcs8",
|
|
64
|
+
serverKeys.privateKey,
|
|
65
|
+
);
|
|
66
|
+
const serverKeyPem = pemEncode(exportedPrivateKey, "PRIVATE KEY");
|
|
67
|
+
const caCertPem = pemEncode(caCertDer, "CERTIFICATE");
|
|
68
|
+
const serverCertPem = pemEncode(serverCertDer, "CERTIFICATE");
|
|
88
69
|
|
|
89
70
|
return {
|
|
90
71
|
caCert: caCertPem,
|
|
91
72
|
key: serverKeyPem,
|
|
92
73
|
cert: serverCertPem,
|
|
93
|
-
fullChain: `${serverCertPem}\n${caCertPem}`,
|
|
74
|
+
fullChain: `${serverCertPem}\n${caCertPem}`,
|
|
94
75
|
};
|
|
95
76
|
}
|
|
77
|
+
|
|
78
|
+
/* ====================================================================
|
|
79
|
+
Pure ASN.1 / DER Encoders (Zero-Dependency X.509 Construction)
|
|
80
|
+
==================================================================== */
|
|
81
|
+
|
|
82
|
+
interface CertOptions {
|
|
83
|
+
serialNumber: number;
|
|
84
|
+
subjectCN: string;
|
|
85
|
+
issuerCN: string;
|
|
86
|
+
notBefore: Date;
|
|
87
|
+
notAfter: Date;
|
|
88
|
+
publicKey: CryptoKey;
|
|
89
|
+
signingPrivateKey: CryptoKey;
|
|
90
|
+
isCA: boolean;
|
|
91
|
+
sans?: Array<{ type: "dns" | "ip"; value: string }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function createX509Cert(opts: CertOptions): Promise<ArrayBuffer> {
|
|
95
|
+
const spkiBuffer = await crypto.subtle.exportKey("spki", opts.publicKey);
|
|
96
|
+
|
|
97
|
+
// Building Extensions
|
|
98
|
+
const extensionsList: Uint8Array[] = [];
|
|
99
|
+
|
|
100
|
+
// 1. Basic Constraints
|
|
101
|
+
const basicConstraintsValue = derSequence([
|
|
102
|
+
...(opts.isCA ? [derBoolean(true)] : []),
|
|
103
|
+
]);
|
|
104
|
+
extensionsList.push(
|
|
105
|
+
derSequence([
|
|
106
|
+
derOid("2.5.29.19"), // id-ce-basicConstraints
|
|
107
|
+
derBoolean(true), // Critical
|
|
108
|
+
derOctetString(basicConstraintsValue),
|
|
109
|
+
]),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// 2. Key Usage (Fixed BitString unused bits padding)
|
|
113
|
+
if (opts.isCA) {
|
|
114
|
+
// keyCertSign (bit 5) | digitalSignature (bit 0) -> 0x84, 1 unused bit in last byte (0x01)
|
|
115
|
+
extensionsList.push(
|
|
116
|
+
derSequence([
|
|
117
|
+
derOid("2.5.29.15"),
|
|
118
|
+
derBoolean(true),
|
|
119
|
+
derOctetString(derBitString(new Uint8Array([0x84]), 1)),
|
|
120
|
+
]),
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
// digitalSignature (bit 0) | keyEncipherment (bit 2) -> 0xa0, 5 unused bits (0x05)
|
|
124
|
+
extensionsList.push(
|
|
125
|
+
derSequence([
|
|
126
|
+
derOid("2.5.29.15"),
|
|
127
|
+
derBoolean(true),
|
|
128
|
+
derOctetString(derBitString(new Uint8Array([0xa0]), 5)),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Extended Key Usage (Required for TLS Server Authentication)
|
|
134
|
+
if (!opts.isCA) {
|
|
135
|
+
const ekuValue = derSequence([
|
|
136
|
+
derOid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
|
|
137
|
+
]);
|
|
138
|
+
extensionsList.push(
|
|
139
|
+
derSequence([
|
|
140
|
+
derOid("2.5.29.37"), // id-ce-extKeyUsage
|
|
141
|
+
derOctetString(ekuValue),
|
|
142
|
+
]),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Subject Alternative Names (SAN)
|
|
147
|
+
if (opts.sans && opts.sans.length > 0) {
|
|
148
|
+
const sanEntries: Uint8Array[] = opts.sans.map((san) => {
|
|
149
|
+
if (san.type === "dns") {
|
|
150
|
+
return derTag(0x82, new TextEncoder().encode(san.value)); // dNSName [2]
|
|
151
|
+
} else {
|
|
152
|
+
const ipParts = san.value.includes(":")
|
|
153
|
+
? parseIPv6(san.value)
|
|
154
|
+
: san.value.split(".").map((v) => parseInt(v, 10));
|
|
155
|
+
return derTag(0x87, new Uint8Array(ipParts)); // iPAddress [7]
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const sanValue = derSequence(sanEntries);
|
|
160
|
+
extensionsList.push(
|
|
161
|
+
derSequence([
|
|
162
|
+
derOid("2.5.29.17"), // id-ce-subjectAltName
|
|
163
|
+
derOctetString(sanValue),
|
|
164
|
+
]),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const extensionsContainer = derTag(0xa3, derSequence(extensionsList));
|
|
169
|
+
|
|
170
|
+
// TBSCertificate (To Be Signed)
|
|
171
|
+
const tbsCert = derSequence([
|
|
172
|
+
derTag(0xa0, derInteger(2)), // Version v3 (2)
|
|
173
|
+
derInteger(opts.serialNumber),
|
|
174
|
+
derSequence([derOid("1.2.840.113549.1.1.11")]), // sha256WithRSAEncryption
|
|
175
|
+
derSequence([
|
|
176
|
+
derSet([
|
|
177
|
+
derSequence([derOid("2.5.4.3"), derPrintableString(opts.issuerCN)]),
|
|
178
|
+
]),
|
|
179
|
+
]),
|
|
180
|
+
derSequence([derUTCTime(opts.notBefore), derUTCTime(opts.notAfter)]),
|
|
181
|
+
derSequence([
|
|
182
|
+
derSet([
|
|
183
|
+
derSequence([derOid("2.5.4.3"), derPrintableString(opts.subjectCN)]),
|
|
184
|
+
]),
|
|
185
|
+
]),
|
|
186
|
+
new Uint8Array(spkiBuffer),
|
|
187
|
+
extensionsContainer,
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// Sign TBSCertificate
|
|
191
|
+
const tbsBuffer = tbsCert.buffer.slice(
|
|
192
|
+
tbsCert.byteOffset,
|
|
193
|
+
tbsCert.byteOffset + tbsCert.byteLength,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const signature = await crypto.subtle.sign(
|
|
197
|
+
{ name: "RSASSA-PKCS1-v1_5" },
|
|
198
|
+
opts.signingPrivateKey,
|
|
199
|
+
tbsBuffer as unknown as ArrayBuffer,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Final X.509 Certificate
|
|
203
|
+
const finalSequence = derSequence([
|
|
204
|
+
tbsCert,
|
|
205
|
+
derSequence([derOid("1.2.840.113549.1.1.11")]),
|
|
206
|
+
derBitString(new Uint8Array(signature), 0),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
return finalSequence.buffer.slice(
|
|
210
|
+
finalSequence.byteOffset,
|
|
211
|
+
finalSequence.byteOffset + finalSequence.byteLength,
|
|
212
|
+
) as unknown as ArrayBuffer;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* --- DER Primitive Encoding Helpers --- */
|
|
216
|
+
|
|
217
|
+
function concatBuffers(bufs: Uint8Array[]): Uint8Array {
|
|
218
|
+
const total = bufs.reduce((sum, b) => sum + b.length, 0);
|
|
219
|
+
const res = new Uint8Array(new ArrayBuffer(total));
|
|
220
|
+
let offset = 0;
|
|
221
|
+
for (const b of bufs) {
|
|
222
|
+
res.set(b, offset);
|
|
223
|
+
offset += b.length;
|
|
224
|
+
}
|
|
225
|
+
return res;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function derTag(tag: number, payload: Uint8Array): Uint8Array {
|
|
229
|
+
const len = payload.length;
|
|
230
|
+
let lenBytes: Uint8Array;
|
|
231
|
+
if (len < 128) {
|
|
232
|
+
lenBytes = new Uint8Array([len]);
|
|
233
|
+
} else if (len < 256) {
|
|
234
|
+
lenBytes = new Uint8Array([0x81, len]);
|
|
235
|
+
} else {
|
|
236
|
+
lenBytes = new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
|
|
237
|
+
}
|
|
238
|
+
return concatBuffers([new Uint8Array([tag]), lenBytes, payload]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function derSequence(items: Uint8Array[]): Uint8Array {
|
|
242
|
+
return derTag(0x30, concatBuffers(items));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function derSet(items: Uint8Array[]): Uint8Array {
|
|
246
|
+
return derTag(0x31, concatBuffers(items));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function derBoolean(val: boolean): Uint8Array {
|
|
250
|
+
return derTag(0x01, new Uint8Array([val ? 0xff : 0x00]));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function derInteger(val: number): Uint8Array {
|
|
254
|
+
return derTag(0x02, new Uint8Array([val]));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function derOctetString(val: Uint8Array): Uint8Array {
|
|
258
|
+
return derTag(0x04, val);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function derPrintableString(val: string): Uint8Array {
|
|
262
|
+
return derTag(0x13, new TextEncoder().encode(val));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function derOid(oidStr: string): Uint8Array {
|
|
266
|
+
const parts = oidStr.split(".").map((v) => parseInt(v, 10));
|
|
267
|
+
const bytes = [parts[0]! * 40 + parts[1]!];
|
|
268
|
+
for (let i = 2; i < parts.length; i++) {
|
|
269
|
+
let v = parts[i] || 0;
|
|
270
|
+
const stack: number[] = [];
|
|
271
|
+
stack.push(v & 0x7f);
|
|
272
|
+
while ((v >>= 7) > 0) {
|
|
273
|
+
stack.push((v & 0x7f) | 0x80);
|
|
274
|
+
}
|
|
275
|
+
bytes.push(...stack.reverse());
|
|
276
|
+
}
|
|
277
|
+
return derTag(0x06, new Uint8Array(bytes));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function derBitString(val: Uint8Array, unusedBits = 0): Uint8Array {
|
|
281
|
+
return derTag(0x03, concatBuffers([new Uint8Array([unusedBits]), val]));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function derUTCTime(date: Date): Uint8Array {
|
|
285
|
+
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
286
|
+
const str = date.getUTCFullYear().toString().slice(-2) +
|
|
287
|
+
pad(date.getUTCMonth() + 1) +
|
|
288
|
+
pad(date.getUTCDate()) +
|
|
289
|
+
pad(date.getUTCHours()) +
|
|
290
|
+
pad(date.getUTCMinutes()) +
|
|
291
|
+
pad(date.getUTCSeconds()) +
|
|
292
|
+
"Z";
|
|
293
|
+
return derTag(0x17, new TextEncoder().encode(str));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function parseIPv6(ip: string): number[] {
|
|
297
|
+
const parts = ip.split("::");
|
|
298
|
+
const head = parts[0] ? parts[0].split(":").map((h) => parseInt(h, 16)) : [];
|
|
299
|
+
const tail = parts[1] ? parts[1].split(":").map((h) => parseInt(h, 16)) : [];
|
|
300
|
+
const missing = 8 - (head.length + tail.length);
|
|
301
|
+
const words = [...head, ...Array(missing).fill(0), ...tail];
|
|
302
|
+
const bytes: number[] = [];
|
|
303
|
+
for (const w of words) {
|
|
304
|
+
bytes.push((w >> 8) & 0xff, w & 0xff);
|
|
305
|
+
}
|
|
306
|
+
return bytes;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function pemEncode(buffer: ArrayBuffer, label: string): string {
|
|
310
|
+
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
311
|
+
const lines = base64.match(/.{1,64}/g) || [];
|
|
312
|
+
return `-----BEGIN ${label}-----\n${
|
|
313
|
+
lines.join("\n")
|
|
314
|
+
}\n-----END ${label}-----`;
|
|
315
|
+
}
|
package/dev_utils/mockConn.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
|
|
2
|
+
import { createAsyncStreamPair } from "./asyncStreamPair.ts";
|
|
3
|
+
import { AsyncStreamConnNext } from "./asyncStreamToNext.ts";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Creates an in-memory mock ConnNext object for unit testing.
|
|
@@ -12,21 +14,8 @@ export function createMockConn(
|
|
|
12
14
|
writerBuf: Uint8Array,
|
|
13
15
|
close = () => {},
|
|
14
16
|
): ConnNext {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let writerPos = 0;
|
|
18
|
-
let isClosed = false;
|
|
19
|
-
|
|
20
|
-
// Helper function to calculate total remaining bytes across readerBuffs
|
|
21
|
-
const getRemainingBytes = (): number => {
|
|
22
|
-
let total = 0;
|
|
23
|
-
for (let i = readIndex; i < readerBuffs.length; i++) {
|
|
24
|
-
total += i === readIndex
|
|
25
|
-
? readerBuffs[i].byteLength - readOffset
|
|
26
|
-
: readerBuffs[i].byteLength;
|
|
27
|
-
}
|
|
28
|
-
return total;
|
|
29
|
-
};
|
|
17
|
+
// Maak een async stream pair aan
|
|
18
|
+
const { input, output } = createAsyncStreamPair();
|
|
30
19
|
|
|
31
20
|
const mockAddr: ConnAddr = {
|
|
32
21
|
transport: "tcp",
|
|
@@ -34,89 +23,44 @@ export function createMockConn(
|
|
|
34
23
|
port: 1883,
|
|
35
24
|
};
|
|
36
25
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* Read exactly 'length' bytes from the provided readerBuffs
|
|
43
|
-
*/
|
|
44
|
-
read(length?: number): Promise<Uint8Array | null> {
|
|
45
|
-
if (isClosed) return Promise.resolve(null);
|
|
46
|
-
|
|
47
|
-
// No specific length requested: return the next available chunk immediately
|
|
48
|
-
if (length === undefined) {
|
|
49
|
-
if (readIndex >= readerBuffs.length) Promise.resolve(null);
|
|
50
|
-
const chunk = readerBuffs[readIndex].subarray(readOffset);
|
|
51
|
-
readIndex++;
|
|
52
|
-
readOffset = 0;
|
|
53
|
-
Promise.resolve(chunk);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
length = length ?? 0;
|
|
57
|
-
|
|
58
|
-
// EOF reached / not enough remaining bytes in the mock buffers
|
|
59
|
-
if (getRemainingBytes() < length) {
|
|
60
|
-
return Promise.resolve(null);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const result = new Uint8Array(length);
|
|
64
|
-
let bytesCopied = 0;
|
|
65
|
-
|
|
66
|
-
while (bytesCopied < length && readIndex < readerBuffs.length) {
|
|
67
|
-
const current = readerBuffs[readIndex];
|
|
68
|
-
const available = current.byteLength - readOffset;
|
|
69
|
-
const needed = length - bytesCopied;
|
|
70
|
-
|
|
71
|
-
if (available <= needed) {
|
|
72
|
-
result.set(current.subarray(readOffset), bytesCopied);
|
|
73
|
-
bytesCopied += available;
|
|
74
|
-
readIndex++;
|
|
75
|
-
readOffset = 0;
|
|
76
|
-
} else {
|
|
77
|
-
result.set(
|
|
78
|
-
current.subarray(readOffset, readOffset + needed),
|
|
79
|
-
bytesCopied,
|
|
80
|
-
);
|
|
81
|
-
bytesCopied += needed;
|
|
82
|
-
readOffset += needed;
|
|
83
|
-
}
|
|
26
|
+
// Push all input directly to the input of the StreamPair
|
|
27
|
+
(async () => {
|
|
28
|
+
try {
|
|
29
|
+
for (const buf of readerBuffs) {
|
|
30
|
+
await input.write(buf);
|
|
84
31
|
}
|
|
32
|
+
input.close();
|
|
33
|
+
} catch {
|
|
34
|
+
// ignore errors
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
85
37
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (isClosed) {
|
|
94
|
-
throw new Error("Connection closed");
|
|
38
|
+
// Capture written data and store it in the write buffer
|
|
39
|
+
(async () => {
|
|
40
|
+
try {
|
|
41
|
+
let writerPos = 0;
|
|
42
|
+
for await (const chunk of input.readable) {
|
|
43
|
+
writerBuf.set(chunk, writerPos);
|
|
44
|
+
writerPos += chunk.byteLength;
|
|
95
45
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore errors
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
100
50
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*/
|
|
104
|
-
close(): void {
|
|
105
|
-
if (!isClosed) {
|
|
106
|
-
isClosed = true;
|
|
107
|
-
close();
|
|
108
|
-
}
|
|
109
|
-
},
|
|
51
|
+
// Return a ConnNext wrapper connected to the output side
|
|
52
|
+
const wrapper = new AsyncStreamConnNext(output);
|
|
110
53
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
54
|
+
return {
|
|
55
|
+
remoteAddr: mockAddr,
|
|
56
|
+
localAddr: mockAddr,
|
|
57
|
+
read: (length) => wrapper.read(length),
|
|
58
|
+
write: (data) => wrapper.write(data),
|
|
59
|
+
[Symbol.asyncIterator]: () => wrapper[Symbol.asyncIterator](),
|
|
60
|
+
close: () => {
|
|
61
|
+
wrapper.close();
|
|
62
|
+
output.close();
|
|
63
|
+
close();
|
|
120
64
|
},
|
|
121
65
|
};
|
|
122
66
|
}
|
package/dev_utils/mockServer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createConnNextPair } from "./
|
|
1
|
+
import { createConnNextPair } from "./asyncStreamPairToNext.ts";
|
|
2
2
|
import { MqttConn } from "../mqttConn/mqttConn.ts";
|
|
3
3
|
import { Context, MqttServer } from "../server/mod.ts";
|
|
4
4
|
import { handlers } from "./test-handlers.ts";
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
readable: ReadableStream<Uint8Array>;
|
|
3
|
-
writable: WritableStream<Uint8Array>;
|
|
4
|
-
close: () => void;
|
|
5
|
-
}
|
|
1
|
+
import type { WebStreamEndpoint } from "./WebStreamEndpoint.ts";
|
|
6
2
|
|
|
7
3
|
export function createWebStreamPair(): {
|
|
8
|
-
input:
|
|
9
|
-
output:
|
|
4
|
+
input: WebStreamEndpoint;
|
|
5
|
+
output: WebStreamEndpoint;
|
|
10
6
|
} {
|
|
11
7
|
let aController: ReadableStreamDefaultController | null = null;
|
|
12
8
|
let bController: ReadableStreamDefaultController | null = null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
|
|
2
|
+
import { createWebStreamPair } from "./webStreamPair.ts";
|
|
3
|
+
import { webStreamToNext } from "./webStreamToNext.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a pair of cross-connected ConnNext sockets backed by WebStreams.
|
|
7
|
+
*/
|
|
8
|
+
export function createConnNextPair(): {
|
|
9
|
+
input: ConnNext;
|
|
10
|
+
output: ConnNext;
|
|
11
|
+
} {
|
|
12
|
+
const { input: webInput, output: webOutput } = createWebStreamPair();
|
|
13
|
+
|
|
14
|
+
const mockAddr: ConnAddr = {
|
|
15
|
+
transport: "tcp",
|
|
16
|
+
hostname: "127.0.0.1",
|
|
17
|
+
port: 1883,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const inputWrapper = webStreamToNext(webInput);
|
|
21
|
+
const outputWrapper = webStreamToNext(webOutput);
|
|
22
|
+
|
|
23
|
+
const input: ConnNext = {
|
|
24
|
+
remoteAddr: mockAddr,
|
|
25
|
+
localAddr: mockAddr,
|
|
26
|
+
read: (length) => inputWrapper.read(length),
|
|
27
|
+
write: (data) => inputWrapper.write(data),
|
|
28
|
+
[Symbol.asyncIterator]: () => inputWrapper[Symbol.asyncIterator](),
|
|
29
|
+
close: () => {
|
|
30
|
+
inputWrapper.close();
|
|
31
|
+
outputWrapper.close();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const output: ConnNext = {
|
|
36
|
+
remoteAddr: mockAddr,
|
|
37
|
+
localAddr: mockAddr,
|
|
38
|
+
read: (length) => outputWrapper.read(length),
|
|
39
|
+
write: (data) => outputWrapper.write(data),
|
|
40
|
+
[Symbol.asyncIterator]: () => outputWrapper[Symbol.asyncIterator](),
|
|
41
|
+
close: () => {
|
|
42
|
+
outputWrapper.close();
|
|
43
|
+
inputWrapper.close();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return { input, output };
|
|
48
|
+
}
|